// ==UserScript== // @name jjg-pdf-downloader // @namespace https://jjg.spc.org.cn/ // @version 0.2.0 // @description Intercept PDF ArrayBuffer and enable Ctrl+S download // @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; 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; }; const toast = (message, type = 'info') => { const container = ensureToastContainer(); const el = document.createElement('div'); const typeStyles = { success: { bg: '#16a34a', border: '#15803d', }, warning: { bg: '#d97706', border: '#b45309', }, error: { bg: '#dc2626', border: '#b91c1c', }, info: { bg: '#2563eb', border: '#1d4ed8', }, }; const style = typeStyles[type] ?? typeStyles.info; el.textContent = message; Object.assign(el.style, { minWidth: '240px', maxWidth: '420px', padding: '12px 16px', borderRadius: '12px', background: style.bg, border: `1px solid ${style.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', pointerEvents: 'none', 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); }; const revokeCurrentUrl = () => { if (currentObjectUrl) { URL.revokeObjectURL(currentObjectUrl); currentObjectUrl = null; } }; const createPdfObjectUrl = (arrayBuffer) => { revokeCurrentUrl(); const blob = new Blob([arrayBuffer], { type: 'application/pdf', }); currentObjectUrl = URL.createObjectURL(blob); toast( 'PDF 已准备好,可按 Ctrl+S 下载', 'success' ); }; const isOnlineReadingRequest = (url) => { try { const parsed = new URL(url, location.href); return parsed.pathname.endsWith('/onlinereading'); } catch { return false; } }; const originalOpen = XMLHttpRequest.prototype.open; const originalSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (...args) { const [, url] = args; this.__jjgPdfDownloaderMatched = typeof url === 'string' && isOnlineReadingRequest(url); return originalOpen.apply(this, args); }; XMLHttpRequest.prototype.send = function (...args) { if (this.__jjgPdfDownloaderMatched) { this.responseType = 'arraybuffer'; this.addEventListener('load', () => { try { if ( this.status >= 200 && this.status < 300 && this.response instanceof ArrayBuffer ) { createPdfObjectUrl(this.response); } } catch (err) { console.error('[jjg-pdf-downloader]', err); toast( 'PDF 捕获失败', 'error' ); } }); } return originalSend.apply(this, args); }; 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' ); }; document.addEventListener( 'keydown', (event) => { const isCtrlS = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's'; if (!isCtrlS) return; event.preventDefault(); event.stopPropagation(); triggerDownload(); }, true ); window.addEventListener( 'beforeunload', revokeCurrentUrl ); })();