您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
添加按钮来复制DOI、获取文献的BibTeX引用格式并下载PDF
- // ==UserScript==
- // @name DOI & BibTeX & PDF Plugin
- // @name:zh-CN DOI和BibTeX和PDF下载插件
- // @description Adds buttons to copy DOI, fetch BibTeX citation, and download PDF from literature pages
- // @description:zh-CN 添加按钮来复制DOI、获取文献的BibTeX引用格式并下载PDF
- // @version 0.2 // Incremented version to reflect styling changes
- // @author Yul
- // @license MIT License
- // @grant GM_setClipboard
- // @grant GM_addStyle
- // @grant GM_xmlhttpRequest
- // @grant GM_download
- // @run-at document-end
- // @match *://www.astm.org/*
- // @match *://www.scirp.org/journal/*
- // @match *://direct.mit.edu/neco/*
- // @match *://ieeexplore.ieee.org/*document/*
- // @match *://ascelibrary.org/doi/*
- // @match *://nhess.copernicus.org/articles/*
- // @match *://www.cambridge.org/core/journals/*
- // @match *://www.mdpi.com/*
- // @match *://en.cgsjournals.com/article/doi/*
- // @match *://adgeo.copernicus.org/articles/*
- // @match *://papers.ssrn.com/*
- // @match *://www.sciencedirect.com/science/article/*
- // @match *://onlinelibrary.wiley.com/doi/*
- // @match *://*.onlinelibrary.wiley.com/doi/*
- // @match *://pubs.acs.org/doi/*
- // @match *://www.tandfonline.com/doi/*
- // @match *://www.beilstein-journals.org/*
- // @match *://www.eurekaselect.com/*/article*
- // @match *://*.springeropen.com/article*
- // @match *://aip.scitation.org/doi/*
- // @match *://www.nature.com/articles*
- // @match *://*.sciencemag.org/content*
- // @match *://journals.aps.org/*/abstract/10*
- // @match *://www.nrcresearchpress.com/doi/10*
- // @match *://iopscience.iop.org/article/10*
- // @match *://www.cell.com/*/fulltext/*
- // @match *://journals.lww.com/*
- // @match *://*.biomedcentral.com/articles/*
- // @match *://journals.sagepub.com/doi/*
- // @match *://academic.oup.com/*/article/*
- // @match *://www.karger.com/Article/*
- // @match *://www.cambridge.org/core/journals/*/article/*
- // @match *://www.annualreviews.org/doi/*
- // @match *://www.jstage.jst.go.jp/article/*
- // @match *://www.hindawi.com/journals/*
- // @match *://www.cardiology.theclinics.com/article/*
- // @match *://www.liebertpub.com/doi/*
- // @match *://thorax.bmj.com/content/*
- // @match *://journals.physiology.org/doi/*
- // @match *://www.ahajournals.org/doi/*
- // @match *://dl.acm.org/doi/*
- // @match *://*.asm.org/content/*
- // @match *://content.apa.org/*
- // @match *://www.thelancet.com/journals/*/article/*
- // @match *://jamanetwork.com/journals/*
- // @match *://*.aacrjournals.org/content/*
- // @match *://royalsocietypublishing.org/doi/*
- // @match *://journals.plos.org/*/article*
- // @match *://*.psychiatryonline.org/doi/*
- // @match *://www.osapublishing.org/*/abstract.cfm*
- // @match *://www.thieme-connect.de/products/ejournals/*
- // @match *://journals.ametsoc.org/*/article/*
- // @match *://www.frontiersin.org/articles/*
- // @match *://www.worldscientific.com/doi/*
- // @match *://www.nejm.org/doi/*
- // @match *://ascopubs.org/doi/*
- // @match *://www.jto.org/article/*
- // @match *://www.jci.org/articles/*
- // @match *://pubmed.ncbi.nlm.nih.gov/*
- // @match *://www.spiedigitallibrary.org/conference-*
- // @match *://www.ingentaconnect.com/content/*
- // @match *://www.taylorfrancis.com/*
- // @match *://www.science.org/doi/*
- // @match *://www.scinapse.io/papers/*
- // @match *://www.semanticscholar.org/paper/*
- // @match *://www.researchgate.net/publication/*
- // @match *://www.earthdoc.org/content/papers/*
- // @match *://era.library.ualberta.ca/items*
- // @match *://arxiv.org/abs/*
- // @match *://asmedigitalcollection.asme.org/IPC*
- // @match *://open.library.ubc.ca/soa/cIRcle/collections/*
- // @match *://pubs.geoscienceworld.org/aeg/eeg/article/*
- // @match *://othes.univie.ac.at/*
- // @match *://www.atlantis-press.com/journals/*
- // @match *://www.koreascience.or.kr/article/*
- // @match *://www.geenmedical.com/article*
- // @match *://www.ncbi.nlm.nih.gov/pmc/articles/*
- // @match *://qjegh.lyellcollection.org/content/*
- // @match *://cdnsciencepub.com/doi/*
- // @match *://ojs.aaai.org//index.php/AAAI/article/*
- // @match *://www.ijcai.org/proceedings/*
- // @match *://www.scopus.com/record/display.uri*
- // @match *://avs.scitation.org/doi/*
- // @match *://pubs.rsc.org/*/content/*
- // @match *://*.copernicus.org/articles/*
- // @match *://europepmc.org/article/*
- // @match *://www.futuremedicine.com/doi/*
- // @include /^http[s]?:\/\/[\S\s]*webofscience[\S\s]+$/
- // @include /^http[s]?:\/\/[\S\s]*springer[\S\s]*/(article|chapter)/
- // @include /^http[s]?:\/\/[\S\s]*onepetro.org/[\S\s]+/(article|proceedings)/
- // @namespace https://greasyfork.org/users/1479737
- // ==/UserScript==
- (function() {
- 'use strict';
- // Configuration Constants
- const CONFIG = {
- MAX_RETRY: 15,
- RETRY_INTERVAL: 300,
- FEEDBACK_DURATION: 2000,
- BIBTEX_TIMEOUT: 10000,
- PDF_TIMEOUT: 15000, // For PDF access checks and other PDF operations
- };
- // BibTeX API Services
- const BIBTEX_APIS = [
- {
- name: 'CrossRef',
- url: (doi) => `https://api.crossref.org/works/${doi}/transform/application/x-bibtex`,
- headers: {'Accept': 'application/x-bibtex'}
- },
- {
- name: 'DOI.org',
- url: (doi) => `https://doi.org/${doi}`,
- headers: {'Accept': 'application/x-bibtex'}
- },
- {
- name: 'Crosscite',
- url: (doi) => `https://citation.crosscite.org/format?doi=${doi}&style=bibtex&lang=en-US`,
- headers: {'Accept': 'text/plain'}
- }
- ];
- // PDF Download Link Selectors (prioritized)
- const PDF_SELECTORS = {
- 'generic': [
- 'a[href$=".pdf"]', 'a[href*="/pdf/"]', 'a[href*="pdf"]',
- 'a[title*="PDF" i]', 'a[title*="Download" i]',
- '.pdf-download a', '.download-pdf a', '.full-text-pdf a'
- ],
- 'specific': {
- 'www.nature.com': ['a[data-track-action="download pdf"]', 'a[href*="/pdf/"]', '.c-pdf-download__link'],
- 'ieeexplore.ieee.org': ['a[href*="stamp.jsp"]', '.pdf-btn a', 'a[href*="arnumber"][href*="pdf"]'],
- 'www.sciencedirect.com': ['a[pdfurl]', '.PdfDownloadButton', 'a[href*="pdfft"]', 'a[data-testid="pdf-link"]'],
- 'onlinelibrary.wiley.com': ['a[href*="pdf"]', '.doi-access a', '.pdf-download-btn', 'a[title*="PDF" i]'],
- 'link.springer.com': ['a[href*="pdf"]', '.pdf-link', '.c-pdf-download__link', 'a[data-track="click_pdf"]'],
- 'journals.plos.org': ['a[id*="downloadPdf"]', '.download a[href*="pdf"]', 'a[href*="article/file"]'],
- 'www.ncbi.nlm.nih.gov': ['a[href*="/pmc/articles/"][href*="/pdf/"]', '.pdf-link', 'a[title*="PDF" i]'],
- 'pubmed.ncbi.nlm.nih.gov': ['.full-text-links a[href*="pdf"]', 'a[data-ga-action="full_text"]'],
- 'journals.aps.org': ['a[href*="/pdf/"]', '.article-nav a[href*="pdf"]'],
- 'iopscience.iop.org': ['a[href*="/pdf/"]', '.pdf-download a'],
- 'www.tandfonline.com': ['a[href*="pdf"]', '.show-pdf a'],
- 'pubs.acs.org': ['a[href*="pdf"]', '.article-pdfLink'],
- 'academic.oup.com': ['a[href*="pdf"]', '.al-link-pdf'],
- 'www.mdpi.com': ['a[href*="pdf"]', '.download-pdf a'],
- 'www.frontiersin.org': ['a[href*="pdf"]', '.download-files a[href*="pdf"]']
- }
- };
- // Site-Specific PDF URL Smart Extractors
- const SITE_SPECIFIC_PDF_EXTRACTORS = {
- 'arxiv.org': () => window.location.pathname.match(/\/abs\/(.+)/) ? `https://arxiv.org/pdf/${window.location.pathname.match(/\/abs\/(.+)/)[1]}.pdf` : null,
- 'www.nature.com': () => {
- const pdfLink = document.querySelector('a[data-track-action="download pdf"]');
- if (pdfLink) return pdfLink.href;
- const articleMatch = window.location.pathname.match(/\/articles\/([^\/]+)/);
- return articleMatch ? `https://www.nature.com/articles/${articleMatch[1]}.pdf` : null;
- },
- 'www.sciencedirect.com': () => {
- const pdfBtn = document.querySelector('a[pdfurl]');
- if (pdfBtn) return pdfBtn.getAttribute('pdfurl');
- const scripts = document.querySelectorAll('script[type="application/json"]');
- for (const script of scripts) {
- try { if (JSON.parse(script.textContent)?.article?.pdfUrl) return JSON.parse(script.textContent).article.pdfUrl; } catch (e) { continue; }
- }
- return null;
- },
- 'ieeexplore.ieee.org': () => {
- const stampLink = document.querySelector('a[href*="stamp.jsp"]');
- if (stampLink) return stampLink.href;
- const arnumberMatch = window.location.search.match(/arnumber=(\d+)/);
- return arnumberMatch ? `https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=${arnumberMatch[1]}` : null;
- },
- 'onlinelibrary.wiley.com': () => {
- const pdfLink = document.querySelector('a[href*="pdf"][title*="PDF"]'); // More specific
- if (pdfLink) return pdfLink.href;
- const doiMatch = window.location.pathname.match(/\/doi\/(10\..+)/);
- return doiMatch ? `https://onlinelibrary.wiley.com/doi/pdf/${doiMatch[1]}` : null;
- },
- 'www.ncbi.nlm.nih.gov': () => {
- if (window.location.pathname.includes('/pmc/articles/')) {
- const pmcMatch = window.location.pathname.match(/(PMC\d+)/);
- return pmcMatch ? `https://www.ncbi.nlm.nih.gov/pmc/articles/${pmcMatch[1]}/pdf/` : null;
- }
- return null;
- },
- 'journals.plos.org': () => {
- const pdfLink = document.querySelector('a[id*="downloadPdf"]');
- if (pdfLink) return pdfLink.href;
- const doiMatch = window.location.search.match(/id=(10\..+)/); // Assuming ID is DOI
- return doiMatch ? `https://journals.plos.org/plosone/article/file?id=${doiMatch[1]}&type=printable` : null;
- },
- 'link.springer.com': () => {
- const pdfLink = document.querySelector('a.c-pdf-download__link, a[data-track="click_pdf"][href*="pdf"]');
- if (pdfLink) return pdfLink.href;
- const doiMatch = window.location.pathname.match(/\/(10\.[^/]+\/[^/]+)/);
- return doiMatch ? `https://link.springer.com/content/pdf/${doiMatch[1]}.pdf` : null;
- }
- };
- // State Management
- let state = {
- timer: null,
- retryCount: 0,
- currentDOI: null,
- currentPDFUrl: null,
- widget: null,
- bibtexCache: new Map(),
- pdfCache: new Map()
- };
- // DOI Extraction Rules
- const DOI_SELECTORS = [
- 'meta[name="citation_doi"]', 'meta[name="dc.identifier"][scheme="DOI"]',
- 'meta[name="dc.Identifier"][scheme="DOI"]', 'meta[name="DC.identifier"][scheme="DOI"]',
- 'meta[name="dc.identifier"]', 'meta[name="dc.Identifier"]', 'meta[name="DC.identifier"]',
- 'meta[property="og:url"]'
- ];
- const SITE_SPECIFIC_EXTRACTORS = { // For DOI
- 'ieeexplore.ieee.org': () => document.querySelector('div.stats-document-abstract-doi a')?.textContent,
- 'www.sciencedirect.com': () => {
- const script = document.querySelector('script[type="application/json"][data-iso-key="_0"]');
- if (script?.textContent) { try { return JSON.parse(script.textContent)?.article?.doi; } catch (e) { console.warn('Failed to parse ScienceDirect JSON for DOI:', e); } }
- },
- 'www.researchgate.net': () => document.querySelector("div.research-detail-meta-item a[href*='doi.org/10.'], div.js-publication-details a[href*='doi.org/10.']")?.href,
- 'www.webofscience.com': () => document.querySelector('#FullRTa-DOI, app-full-record-summary-item[data-test-id="summary-DOI"] .value')?.textContent,
- 'pubmed.ncbi.nlm.nih.gov': () => document.querySelector('span.citation-doi a, a.id-link[data-ga-action="DOI"]')?.textContent,
- 'www.ncbi.nlm.nih.gov': () => window.location.pathname.includes('/pmc/articles/') ? document.querySelector('td.doi a, span.doi a')?.textContent : null,
- 'arxiv.org': () => document.querySelector('td.tablecell.doi a, div.submission-history dt:contains("DOI:") + dd, div.extra-services div.full-text ul li a[href*="doi.org/10."]')?.textContent || document.querySelector('div.extra-services div.full-text ul li a[href*="doi.org/10."]')?.href,
- 'www.semanticscholar.org': () => document.querySelector('span[data-test-id="paper-doi"] a')?.href || document.querySelector('[data-test-id="paper-meta-item"] a[href*="doi.org"]')?.textContent
- };
- // Utility Functions
- const utils = {
- normalizeHostname: (hostname) => {
- if (hostname.includes('webofscience')) return 'www.webofscience.com';
- if (hostname.includes('springer.com') || hostname.includes('springerlink.com')) return 'link.springer.com';
- if (hostname.includes('onlinelibrary.wiley.com')) return 'onlinelibrary.wiley.com';
- return hostname;
- },
- normalizeDOI: (doi) => {
- if (!doi) return null;
- let normalized = decodeURIComponent(doi.toString().trim()).replace(/^doi:\s*/i, '').replace(/^https?:\/\/doi\.org\//i, '');
- const strictMatch = normalized.match(/10\.\d{4,9}\/[-._;()/:A-Z0-9]+$/i);
- if (strictMatch) return strictMatch[0];
- const looseMatch = normalized.match(/10\.[^\s]+/);
- return looseMatch && normalized.startsWith("10.") ? looseMatch[0] : null;
- },
- debounce: (func, delay) => {
- let timeoutId;
- return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(null, args), delay); };
- },
- cleanBibTeX: (bibtex) => {
- if (!bibtex) return null;
- return bibtex.replace(/^\s+|\s+$/g, '').replace(/\n\s*\n/g, '\n').replace(/\s+/g, ' ').replace(/,\s*}/g, '\n}').replace(/,\s*([a-zA-Z])/g, ',\n $1');
- },
- generatePDFFilename: (doi, title) => {
- let filename = '';
- if (title) { filename = title.replace(/[^\w\s-]/g, '').replace(/\s+/g, '_').substring(0, 50); }
- else if (doi) { filename = doi.replace(/[/\\:*?"<>|]/g, '_'); }
- else { filename = `paper_${Date.now()}`; }
- return `${filename}.pdf`;
- },
- isValidPDFUrl: (url) => {
- if (!url) return false;
- try { new URL(url, window.location.href); } catch { return false; }
- const excludePatterns = [/\.(jpg|jpeg|png|gif|svg|css|js|html)$/i, /javascript:/i, /mailto:/i, /#$/];
- return !excludePatterns.some(pattern => pattern.test(url));
- },
- checkPDFAccess: async (url) => {
- return new Promise((resolve) => {
- GM_xmlhttpRequest({
- method: 'HEAD', url: url, timeout: CONFIG.PDF_TIMEOUT / 3, // Shorter timeout for HEAD
- onload: (response) => {
- const isAccessible = response.status === 200 || response.status === 302;
- const contentType = response.responseHeaders?.toLowerCase() || "";
- const isPDF = contentType.includes('application/pdf') || url.toLowerCase().endsWith('.pdf');
- resolve(isAccessible && isPDF);
- },
- onerror: () => resolve(false), ontimeout: () => resolve(false)
- });
- });
- }
- };
- // DOI Extractor
- const extractDOI = () => {
- const hostname = utils.normalizeHostname(window.location.hostname);
- const siteExtractor = SITE_SPECIFIC_EXTRACTORS[hostname];
- if (siteExtractor) { const doi = siteExtractor(); if (doi) return utils.normalizeDOI(doi); }
- for (const selector of DOI_SELECTORS) {
- const element = document.querySelector(selector);
- if (element?.content) { const doi = utils.normalizeDOI(element.content); if (doi) return doi; }
- }
- const doiLink = document.querySelector('a[href*="doi.org/10."]');
- if (doiLink?.href) return utils.normalizeDOI(doiLink.href);
- return null;
- };
- // BibTeX Fetching
- const fetchBibTeX = async (doi) => {
- if (state.bibtexCache.has(doi)) return state.bibtexCache.get(doi);
- for (const api of BIBTEX_APIS) {
- try {
- const result = await new Promise((resolve, reject) => {
- const timeout = setTimeout(() => reject(new Error('Timeout')), CONFIG.BIBTEX_TIMEOUT);
- GM_xmlhttpRequest({
- method: 'GET', url: api.url(doi), headers: api.headers, timeout: CONFIG.BIBTEX_TIMEOUT,
- onload: (response) => {
- clearTimeout(timeout);
- if (response.status === 200 && response.responseText) {
- const cleaned = utils.cleanBibTeX(response.responseText);
- if (cleaned && cleaned.includes('@')) resolve(cleaned); else reject(new Error('Invalid BibTeX format'));
- } else reject(new Error(`HTTP ${response.status}`));
- },
- onerror: (error) => { clearTimeout(timeout); reject(error); },
- ontimeout: () => { clearTimeout(timeout); reject(new Error('Request timeout')); }
- });
- });
- state.bibtexCache.set(doi, result); console.log(`BibTeX fetched successfully from ${api.name}`); return result;
- } catch (error) { console.warn(`${api.name} failed:`, error.message); continue; }
- }
- throw new Error('All BibTeX APIs failed');
- };
- // --- PDF Specific Functions ---
- const findBestPDFUrl = async () => {
- const hostname = utils.normalizeHostname(window.location.hostname);
- const candidates = [];
- const sitePdfExtractor = SITE_SPECIFIC_PDF_EXTRACTORS[hostname];
- if (sitePdfExtractor) { const url = sitePdfExtractor(); if (url && utils.isValidPDFUrl(url)) candidates.push({ url, priority: 10, source: 'site-specific-extractor' });}
- const siteSelectors = PDF_SELECTORS.specific[hostname];
- if (siteSelectors) {
- for (let i = 0; i < siteSelectors.length; i++) {
- try {
- document.querySelectorAll(siteSelectors[i]).forEach(el => {
- const url = el.href || el.getAttribute('pdfurl') || el.getAttribute('data-pdf-url');
- if (url && utils.isValidPDFUrl(url)) candidates.push({ url, priority: 9 - i, source: `site-selector: ${siteSelectors[i]}`});
- });
- } catch (e) { console.warn(`PDF selector failed: ${siteSelectors[i]}`, e); }
- }
- }
- PDF_SELECTORS.generic.forEach((selector, index) => {
- try {
- document.querySelectorAll(selector).forEach(el => {
- const url = el.href || el.getAttribute('pdfurl');
- if (url && utils.isValidPDFUrl(url)) {
- let priority = 6 - index;
- if (url.toLowerCase().includes('pdf') || url.endsWith('.pdf')) priority += 2;
- const text = el.textContent?.toLowerCase() || '';
- if (text.includes('pdf') || text.includes('download')) priority += 1;
- candidates.push({ url, priority, source: `generic: ${selector}`});
- }
- });
- } catch (e) { console.warn(`Generic PDF selector failed: ${selector}`, e); }
- });
- if (state.currentDOI) {
- generateDOIBasedPDFUrls(state.currentDOI).forEach(url => candidates.push({ url, priority: 3, source: 'doi-based' }));
- }
- candidates.sort((a, b) => b.priority - a.priority);
- const uniqueCandidates = Array.from(new Map(candidates.map(c => [c.url, c])).values());
- console.log('PDF candidates found:', uniqueCandidates.map(c => `${c.url} (P:${c.priority}, S:${c.source})`));
- for (const candidate of uniqueCandidates.slice(0, 5)) { // Check top 5
- if (await utils.checkPDFAccess(candidate.url)) { console.log(`Best PDF URL selected (accessible): ${candidate.url} (${candidate.source})`); return candidate.url; }
- }
- return uniqueCandidates.length > 0 ? uniqueCandidates[0].url : null; // Fallback to highest priority if none are confirmed accessible
- };
- const generateDOIBasedPDFUrls = (doi) => { // Simplified
- if (!doi) return [];
- const doiParts = doi.split('/');
- const suffix = doiParts.length > 1 ? doiParts[1] : doi;
- return [
- `https://www.nature.com/articles/${suffix}.pdf`,
- `https://link.springer.com/content/pdf/${doi}.pdf`,
- `https://onlinelibrary.wiley.com/doi/pdf/${doi}`
- // Add more patterns if known and reliable
- ].filter(url => utils.isValidPDFUrl(url));
- };
- const deepScanForPDF = () => {
- const potentialLinks = [];
- document.querySelectorAll('a[href]').forEach(link => {
- const href = link.href.toLowerCase(); const text = link.textContent.toLowerCase();
- const pdfKeywords = ['pdf', 'download', 'full text', 'full-text', 'view pdf', 'get pdf'];
- if (pdfKeywords.some(k => href.includes(k) || text.includes(k)) && utils.isValidPDFUrl(link.href)) {
- let score = 1;
- if (href.endsWith('.pdf')) score += 5; if (href.includes('/pdf/')) score += 3;
- if (text.includes('pdf')) score += 2; if (text.includes('download')) score += 2;
- if (link.download) score += 3;
- potentialLinks.push({ url: link.href, score });
- }
- });
- potentialLinks.sort((a, b) => b.score - a.score);
- return potentialLinks.length > 0 ? potentialLinks[0].url : null;
- };
- const heuristicPDFSearch = async () => {
- const embeds = document.querySelectorAll('embed[src*="pdf"], object[data*="pdf"], iframe[src*="pdf"]');
- for (const embed of embeds) { const src = embed.src || embed.data; if (src && utils.isValidPDFUrl(src)) return src; }
- const scripts = document.querySelectorAll('script:not([src])');
- for (const script of scripts) {
- const text = script.textContent; const pdfMatches = text.match(/["'](https?:\/\/[^"']*\.pdf[^"']*?)["']/gi);
- if (pdfMatches) { for (const match of pdfMatches) { const url = match.slice(1, -1); if (utils.isValidPDFUrl(url)) return url; } }
- }
- const dataElements = document.querySelectorAll('[data-pdf-url], [data-download-url], [data-file-url]');
- for (const el of dataElements) { const url = el.dataset.pdfUrl || el.dataset.downloadUrl || el.dataset.fileUrl; if (url && utils.isValidPDFUrl(url)) return url; }
- return null;
- };
- const downloadPDF = async (pdfUrl, button) => {
- if (!pdfUrl) { showCopyFeedback(button, '无PDF链接', true); return; }
- try {
- const title = document.querySelector('meta[name="citation_title"]')?.content || document.querySelector('h1')?.textContent || document.title;
- const filename = utils.generatePDFFilename(state.currentDOI, title);
- if (typeof GM_download !== 'undefined') {
- GM_download({ url: pdfUrl, name: filename, saveAs: true }); // saveAs: true to prompt user
- showCopyFeedback(button, '下载中...', false); console.log(`Downloading PDF: ${pdfUrl} as ${filename}`); return;
- }
- const link = document.createElement('a'); link.href = pdfUrl; link.download = filename; link.target = '_blank';
- document.body.appendChild(link); link.click(); document.body.removeChild(link);
- showCopyFeedback(button, '已启动下载', false); console.log(`PDF download initiated: ${pdfUrl}`);
- } catch (error) { console.error('PDF download failed:', error); showCopyFeedback(button, '下载失败', true); }
- };
- const enhancedDownloadPDF = async (button) => {
- const originalText = button.textContent;
- button.textContent = 'PDF'; button.disabled = true;
- try {
- let pdfUrl = state.currentPDFUrl || state.pdfCache.get(window.location.href);
- if (!pdfUrl) pdfUrl = await findBestPDFUrl();
- if (!pdfUrl) pdfUrl = deepScanForPDF();
- if (!pdfUrl) pdfUrl = await heuristicPDFSearch();
- if (pdfUrl) {
- state.currentPDFUrl = pdfUrl; state.pdfCache.set(window.location.href, pdfUrl);
- await downloadPDF(pdfUrl, button); // This will call showCopyFeedback
- } else {
- showCopyFeedback(button, '未找到PDF', true); // This restores button
- }
- } catch (error) {
- console.error('Enhanced PDF download failed:', error);
- showCopyFeedback(button, 'PDF错误', true); // This restores button
- }
- // If showCopyFeedback was called, button state is handled.
- // If an error occurred before showCopyFeedback, or if it didn't restore for some reason:
- if (button.textContent === 'PDF') { // Check if it's still in intermediate state
- button.textContent = originalText;
- button.disabled = false;
- }
- };
- // --- End PDF Specific Functions ---
- // Clipboard Operations
- const copyToClipboard = async (text, button, successMsg = 'Copied') => {
- try {
- if (typeof GM_setClipboard !== 'undefined') { GM_setClipboard(text); showCopyFeedback(button, successMsg); return; }
- if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); showCopyFeedback(button, successMsg); return; }
- throw new Error('No clipboard API available');
- } catch (error) { console.error('Failed to copy:', error); showCopyFeedback(button, '复制失败', true); }
- };
- // Feedback Display
- const showCopyFeedback = (button, message, isError = false) => {
- if (!button) return;
- const originalText = button.dataset.originalText || button.textContent; // Store original text if not already
- if (!button.dataset.originalText) button.dataset.originalText = button.textContent;
- button.textContent = message;
- const baseClass = button.className.replace(/ copied-feedback| error-feedback/g, '');
- button.className = baseClass + (isError ? ' error-feedback' : ' copied-feedback');
- button.disabled = true;
- setTimeout(() => {
- button.textContent = originalText;
- button.className = baseClass;
- button.disabled = false;
- delete button.dataset.originalText; // Clean up
- }, CONFIG.FEEDBACK_DURATION);
- };
- // BibTeX Button Click Handler
- const handleBibTeXClick = async (button) => {
- if (!state.currentDOI) { showCopyFeedback(button, '无DOI', true); return; }
- const originalText = button.textContent;
- button.textContent = 'BibTeX';
- button.disabled = true;
- try {
- const bibtex = await fetchBibTeX(state.currentDOI);
- await copyToClipboard(bibtex, button, 'BibTeX已复制'); // copyToClipboard calls showCopyFeedback
- } catch (error) {
- console.error('Failed to fetch BibTeX:', error);
- showCopyFeedback(button, '获取失败', true); // This will restore button
- }
- // If button text is still "获取BibTeX...", restore it (e.g. if copyToClipboard failed silently before its own showCopyFeedback)
- if (button.textContent === 'BibTeX') {
- button.textContent = originalText;
- button.disabled = false;
- }
- };
- // UI Styling
- const createStyles = () => {
- GM_addStyle(`
- #doi-widget-container {
- position: fixed; bottom: 20px; right: 20px;
- background-color: #0C344E; border-radius: 12px;
- box-shadow: 0 4px 20px rgba(0,0,0,0.15); z-index: 2147483647;
- display: none; overflow: hidden; text-align: center; color: white;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- min-width: 100px; /* Adjusted min-width for potentially longer text */
- }
- .widget-button {
- display: block; width: 100%; padding: 10px 15px;
- font-size: 14px; font-weight: 600; color: white;
- border: none; cursor: pointer; transition: all 0.2s ease;
- outline: none; border-bottom: 1px solid rgba(255,255,255,0.1);
- }
- .widget-button:last-of-type { border-bottom: none; } /* Applies to last button before copyright */
- #doi-widget-button { background-color: #118ab2; border-radius: 12px 12px 0 0; }
- #doi-widget-button:hover { background-color: #0f7ea1; transform: translateY(-1px); }
- #bibtex-widget-button { background-color: #06d6a0; border-radius: 0; }
- #bibtex-widget-button:hover { background-color: #05c290; transform: translateY(-1px); }
- #pdf-widget-button { background-color: #ff9f1c; border-radius: 0; } /* Orange for PDF */
- #pdf-widget-button:hover { background-color: #e68a00; transform: translateY(-1px); } /* Darker orange */
- .widget-button:active { transform: scale(0.98); }
- .widget-button:disabled { opacity: 0.7; cursor: not-allowed; transform: none; }
- .widget-button.copied-feedback { background-color: #28a745 !important; }
- .widget-button.error-feedback { background-color: #dc3545 !important; }
- #doi-widget-copyright {
- padding: 8px 12px; font-size: 11px; background-color: #0C344E;
- color: #a0d8ef; border-radius: 0 0 12px 12px;
- }
- @media (max-width: 768px) {
- #doi-widget-container { bottom: 15px; right: 15px; min-width: 90px; }
- .widget-button { padding: 10px 12px; font-size: 13px; }
- }
- `);
- };
- // UI Widget Creation
- const createWidget = () => {
- if (document.getElementById('doi-widget-container')) return;
- const container = document.createElement('div'); container.id = 'doi-widget-container';
- const doiButton = document.createElement('button');
- doiButton.id = 'doi-widget-button'; doiButton.className = 'widget-button';
- doiButton.textContent = 'DOI'; doiButton.title = '点击复制 DOI';
- doiButton.addEventListener('click', () => { if (state.currentDOI) copyToClipboard(state.currentDOI, doiButton, 'DOI已复制'); else showCopyFeedback(doiButton, '无DOI', true); });
- const bibtexButton = document.createElement('button');
- bibtexButton.id = 'bibtex-widget-button'; bibtexButton.className = 'widget-button';
- bibtexButton.textContent = 'BibTeX'; bibtexButton.title = '点击获取并复制 BibTeX';
- bibtexButton.addEventListener('click', () => handleBibTeXClick(bibtexButton));
- const pdfButton = document.createElement('button');
- pdfButton.id = 'pdf-widget-button'; pdfButton.className = 'widget-button';
- pdfButton.textContent = 'PDF'; pdfButton.title = '点击下载 PDF';
- pdfButton.addEventListener('click', () => enhancedDownloadPDF(pdfButton));
- const copyright = document.createElement('div');
- copyright.id = 'doi-widget-copyright'; copyright.textContent = `Yul © ${new Date().getFullYear()}`;
- container.appendChild(doiButton); container.appendChild(bibtexButton); container.appendChild(pdfButton);
- container.appendChild(copyright);
- document.body.appendChild(container);
- state.widget = container;
- };
- // Widget Visibility Control
- const showWidget = () => { if (state.widget) state.widget.style.display = 'block'; };
- const hideWidget = () => { if (state.widget) state.widget.style.display = 'none'; };
- // DOI Detection and Widget Activation
- const attemptExtractDOI = () => {
- state.retryCount++; const doi = extractDOI();
- if (doi) {
- clearInterval(state.timer); state.currentDOI = doi; console.log('DOI found:', doi);
- // Pre-fetch PDF URL in background if DOI is found
- findBestPDFUrl().then(pdfUrl => { if (pdfUrl) { state.currentPDFUrl = pdfUrl; state.pdfCache.set(window.location.href, pdfUrl); console.log('PDF URL pre-extracted:', pdfUrl);}});
- showWidget();
- } else if (state.retryCount >= CONFIG.MAX_RETRY) {
- clearInterval(state.timer); console.log('DOI not found after', CONFIG.MAX_RETRY, 'attempts');
- // Optionally show widget even if DOI not found, for PDF download attempts
- // For now, keeping original behavior: hide if no DOI. PDF button will rely on page scan.
- // To enable PDF button even without DOI, call showWidget() here or make it always visible.
- // For this modification, let's keep it tied to DOI presence for consistency with original style.
- // If you want PDF to always be available, remove hideWidget() or call showWidget()
- hideWidget(); // Original behavior
- }
- };
- const resetAndStart = utils.debounce(() => {
- console.log('DOI & BibTeX & PDF Plugin: Initializing/Resetting...');
- if (state.timer) clearInterval(state.timer);
- state.retryCount = 0; state.currentDOI = null; state.currentPDFUrl = null; // Reset PDF URL too
- // Do not hide widget immediately, attemptExtractDOI will manage visibility
- // hideWidget(); // This would hide it on SPA navigation before DOI is found
- state.timer = setInterval(attemptExtractDOI, CONFIG.RETRY_INTERVAL);
- attemptExtractDOI(); // Try once immediately
- }, 250); // Slightly shorter debounce for SPA
- // Navigation and Mutation Observer
- const setupNavigationListener = () => {
- const originalPushState = history.pushState; const originalReplaceState = history.replaceState;
- history.pushState = function(...args) { originalPushState.apply(this, args); resetAndStart(); };
- history.replaceState = function(...args) { originalReplaceState.apply(this, args); resetAndStart(); };
- window.addEventListener('popstate', resetAndStart);
- if (window.MutationObserver) {
- const observer = new MutationObserver(utils.debounce(() => {
- // Only reset if DOI is not found or URL changed significantly
- // This prevents excessive resets on minor DOM changes
- if (!state.currentDOI || state.lastObservedURL !== window.location.href) {
- state.lastObservedURL = window.location.href;
- resetAndStart();
- }
- }, 1000));
- observer.observe(document.body, { childList: true, subtree: true });
- state.lastObservedURL = window.location.href; // Initialize
- }
- };
- // Initialization
- const init = () => {
- createStyles();
- createWidget(); // Creates the widget but it's hidden by default CSS
- setupNavigationListener();
- resetAndStart(); // This will attempt to find DOI and show/hide widget
- };
- // Script Execution Start
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
- })();