// ==UserScript== // @name jjg-pdf-downloader // @namespace https://jjg.spc.org.cn/ // @version 1.0.0 // @description Intercept online-reading PDF and download via Ctrl+S // @match https://jjg.spc.org.cn/resmea/view/stdonline // @run-at document-start // @grant none // @license AGPL-3.0-or-later // ==/UserScript== (() => { 'use strict'; /** @type {string|null} */ let currentObjectUrl = null; /** @type {HTMLDivElement|null} */ let toastContainer = null; /** * Create toast container lazily * @returns {HTMLDivElement} */ const ensureToastContainer = () => { if (toastContainer) { return toastContainer; } toastContainer = document.createElement('div'); Object.assign(toastContainer.style, { position: 'fixed', top: '16px', left: '50%', transform: 'translateX(-50%)', zIndex: '2147483647', display: 'flex', flexDirection: 'column', gap: '10px', pointerEvents: 'none', }); document.documentElement.appendChild(toastContainer); return toastContainer; }; /** * Lightweight toast * @param {string} message * @param {'success'|'warning'|'error'|'info'} [type] */ const toast = (message, type = 'info') => { const container = ensureToastContainer(); const el = document.createElement('div'); const theme = { success: { bg: '#16a34a', border: '#15803d', }, warning: { bg: '#d97706', border: '#b45309', }, error: { bg: '#dc2626', border: '#b91c1c', }, info: { bg: '#2563eb', border: '#1d4ed8', }, }[type]; el.textContent = message; Object.assign(el.style, { minWidth: '240px', maxWidth: '420px', padding: '12px 16px', borderRadius: '12px', background: theme.bg, border: `1px solid ${theme.border}`, color: '#fff', fontSize: '14px', fontFamily: 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', boxShadow: '0 10px 30px rgba(0,0,0,0.18)', backdropFilter: 'blur(8px)', opacity: '0', transform: 'translateY(-8px)', transition: 'opacity 160ms ease, transform 160ms ease', userSelect: 'none', }); container.appendChild(el); requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0)'; }); setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(-8px)'; setTimeout(() => { el.remove(); if (!container.childElementCount) { container.remove(); toastContainer = null; } }, 180); }, 3000); }; /** * Revoke previous ObjectURL */ const revokeCurrentUrl = () => { if (!currentObjectUrl) { return; } URL.revokeObjectURL(currentObjectUrl); currentObjectUrl = null; }; /** * Save PDF blob as downloadable URL * @param {Blob} blob */ const savePdfBlob = (blob) => { revokeCurrentUrl(); currentObjectUrl = URL.createObjectURL(blob); toast( 'PDF 已准备好,可按 Ctrl+S 下载', 'success' ); }; /** * Check target endpoint * @param {string} url * @returns {boolean} */ const isOnlineReadingRequest = (url) => { try { const parsed = new URL(url, location.href); return parsed.pathname.endsWith('/onlinereading'); } catch { return false; } }; const originalOpen = XMLHttpRequest.prototype.open; /** * Safe XHR observation * * IMPORTANT: * - DO NOT modify responseType * - DO NOT modify response * - DO NOT override onload * - Only observe existing behavior */ XMLHttpRequest.prototype.open = function (...args) { const [, url] = args; if ( typeof url === 'string' && isOnlineReadingRequest(url) ) { this.addEventListener('load', () => { try { if ( this.status < 200 || this.status >= 300 ) { return; } const response = this.response; if (!(response instanceof Blob)) { return; } if ( response.type && response.type !== 'application/pdf' ) { return; } savePdfBlob(response); } catch (err) { console.error( '[jjg-pdf-downloader]', err ); toast( 'PDF 捕获失败', 'error' ); } }); } return originalOpen.apply(this, args); }; /** * Trigger PDF download */ const triggerDownload = () => { if (!currentObjectUrl) { toast( '当前没有可下载的 PDF', 'warning' ); return; } const a = document.createElement('a'); a.href = currentObjectUrl; a.download = 'document.pdf'; document.body.appendChild(a); a.click(); a.remove(); toast( 'PDF 下载已触发', 'success' ); }; /** * Ctrl+S handler */ document.addEventListener( 'keydown', (event) => { const isSaveShortcut = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's'; if (!isSaveShortcut) { return; } event.preventDefault(); event.stopPropagation(); triggerDownload(); }, true ); /** * Cleanup */ window.addEventListener( 'beforeunload', revokeCurrentUrl ); })();