您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在 i-learning 2.0 pdf viewer 頁右下角提供「下載 PDF」。
当前为
// ==UserScript== // @name CYCU iLearning PDF downloader // @name:en CYCU iLearning PDF downloader // @namespace https://github.com/Mono0713/CYCU-ilearning-2.0-pdf-downloader // @version 1.0.0 // @description 在 i-learning 2.0 pdf viewer 頁右下角提供「下載 PDF」。 // @description:en Add a “Download PDF” button on iLearning 2.0 pages; prefer pluginfile.php URL, fallback to PDF.js bytes. // @license MIT // @match https://ilearning.cycu.edu.tw/* // @run-at document-start // @grant GM_download // @grant GM_notification // @homepageURL https://github.com/Mono0713/CYCU-ilearning-2.0-pdf-downloader // @supportURL https://github.com/Mono0713/CYCU-ilearning-2.0-pdf-downloader/issues // @icon https://ilearning.cycu.edu.tw/theme/image.php/boost/theme/1/favicon // ==/UserScript== (function () { 'use strict'; // ---------- 小工具 ---------- const log = (...a) => console.log('[iLearn PDF]', ...a); const notify = (text) => { try { GM_notification({ title: 'iLearning PDF', text, timeout: 2500 }); } catch {} log(text); }; const sanitize = (s) => (s || '') .toString() .replace(/[\\/:*?"<>|]+/g, '_') .trim() .slice(0, 120); const fileNameFromURL = (u) => { try { const p = new URL(u, location.href).pathname.split('/'); return decodeURIComponent(p[p.length - 1] || 'document.pdf') || 'document.pdf'; } catch { return 'document.pdf'; } }; const courseTitle = () => { const h1 = document.querySelector('#page-header .page-header-headings h1') || document.querySelector('header h1') || document.querySelector('h1'); const t = (h1 && h1.textContent.trim()) || document.title.replace(/\s+\|.*/, ''); return sanitize(t || 'iLearning'); }; // ---------- 原始 URL 偵測(pluginfile.php) ---------- const found = new Set(); const candidates = []; // 依時間排序的候選 {url, ts} const isPDFUrl = (u) => /\/pluginfile\.php\/.+\.pdf(?:$|\?)/i.test(u) || /\.pdf(?:$|\?)/i.test(u); function recordUrl(url) { if (!url || !isPDFUrl(url) || found.has(url)) return; found.add(url); candidates.push({ url, ts: Date.now() }); // 保留最近 20 筆 if (candidates.length > 20) candidates.splice(0, candidates.length - 20); log('捕捉到 PDF URL:', url); } // 既有資源掃描 + 連結掃描 function initialScan() { try { performance.getEntriesByType('resource') .forEach(e => isPDFUrl(e.name) && recordUrl(e.name)); } catch {} document.querySelectorAll('a[href*="/pluginfile.php/"]').forEach(a => { const href = a.getAttribute('href'); if (href) { try { recordUrl(new URL(href, location.href).href); } catch { recordUrl(href); } } }); } // 持續監聽新資源 try { new PerformanceObserver(list => { for (const e of list.getEntries()) { if (e.entryType === 'resource' && isPDFUrl(e.name)) recordUrl(e.name); } }).observe({ entryTypes: ['resource'] }); } catch {} if (document.readyState === 'loading') { addEventListener('DOMContentLoaded', initialScan, { once: true }); } else { initialScan(); } function latestPdfUrl() { const arr = candidates.slice().sort((a, b) => b.ts - a.ts); const hit = arr.find(x => isPDFUrl(x.url)); return hit && hit.url; } // ---------- PDF.js 直接取 bytes(跨所有 frame) ---------- function findPDFViewerWindow() { const seen = new Set(), q = [window]; while (q.length) { const w = q.shift(); if (seen.has(w)) continue; seen.add(w); try { const app = w.PDFViewerApplication; if (app && app.pdfDocument && typeof app.pdfDocument.getData === 'function') { return w; } } catch {} try { for (let i = 0; i < w.frames.length; i++) q.push(w.frames[i]); } catch {} } return null; } async function getPdfBlobViaPDFJS() { const w = findPDFViewerWindow(); if (!w) return { ok: false, error: 'no-app' }; const app = w.PDFViewerApplication; // 如果有非 blob 的原始 URL,仍優先回傳 URL(更快) try { const url = (app.url || app.appConfig?.defaultUrl) || ''; if (url && !String(url).startsWith('blob:') && isPDFUrl(url)) { return { ok: true, url }; } } catch {} try { const u8 = await app.pdfDocument.getData(); const blob = new Blob([u8], { type: 'application/pdf' }); // 名稱盡量取自 app.url;否則用課程標題 let name = ''; try { if (app.url) name = fileNameFromURL(app.url); } catch {} if (!name) name = courseTitle() + '.pdf'; return { ok: true, blob, name }; } catch (e) { return { ok: false, error: 'getdata-fail' }; } } // ---------- 下載實作 ---------- function downloadURL(url, name) { const n = sanitize(name || fileNameFromURL(url)); if (typeof GM_download === 'function') { GM_download({ url, name: n }); } else { const a = document.createElement('a'); a.href = url; a.download = n; document.body.appendChild(a); a.click(); a.remove(); } notify('下載:' + n); } function downloadBlob(blob, name) { const n = sanitize(name || (courseTitle() + '.pdf')); const url = URL.createObjectURL(blob); if (typeof GM_download === 'function') { GM_download({ url, name: n, onload: () => URL.revokeObjectURL(url), onerror: () => URL.revokeObjectURL(url) }); } else { const a = document.createElement('a'); a.href = url; a.download = n; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } notify('下載:' + n); } // ---------- 主流程(按鈕行為) ---------- async function handleDownload() { // 1) 優先:已抓到的原始 URL const url = latestPdfUrl(); if (url) { return downloadURL(url); } // 2) 退路:直接向 PDF.js 取 bytes const r = await getPdfBlobViaPDFJS(); if (r.ok && r.url) { return downloadURL(r.url); } if (r.ok && r.blob) { return downloadBlob(r.blob, r.name); } // 3) 還是沒有 → 提示使用者觸發載入 alert('尚未偵測到本頁的 PDF。\n請先翻到下一頁或重新整理,再按一次「下載 PDF」。'); } // ---------- 右下角 UI ---------- if (window.top === window) { const ID = 'ilearn-pdf-dl-btn'; if (!document.getElementById(ID)) { const btn = document.createElement('button'); btn.id = ID; btn.textContent = '⬇ 下載 PDF'; Object.assign(btn.style, { position: 'fixed', right: '14px', bottom: '14px', zIndex: 2147483647, padding: '10px 14px', background: '#2563eb', color: '#fff', border: 'none', borderRadius: '10px', boxShadow: '0 6px 16px rgba(0,0,0,.2)', cursor: 'pointer', fontSize: '14px' }); btn.addEventListener('click', handleDownload); document.documentElement.appendChild(btn); } } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址