Scholar Access Enhancer

Add research access buttons to Scholar results (DOI-focused Sci-Hub, Sci-DB, and title-based searches)

// ==UserScript==
// @name         Scholar Access Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Add research access buttons to Scholar results (DOI-focused Sci-Hub, Sci-DB, and title-based searches)
// @author       Adelyn Maisie
// @match        https://scholar.google.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license      AGPLv3
// ==/UserScript==

(function() {
    'use strict';

    // Configuration
    const services = {
        SciDB: {
            name: 'Sci-DB',
            color: '#a83293',
            getUrl: (doi) => doi ? `https://annas-archive.org/scidb/${doi}` : null
        },
        AnnasArchive: {
            name: "Anna's Archive",
            color: '#FF5722',
            getUrl: (title) => title ? `https://annas-archive.org/search?index=journals&q=${encodeURIComponent(title)}` : null
        },
        SciHub: {
            name: 'Sci-Hub',
            color: '#2c7bb6',
            getUrl: (doi) => doi ? `https://sci-hub.st/${doi}` : null
        },
        LibGen: {
            name: 'LibGen',
            color: '#4CAF50',
            getUrl: (title) => title ? `https://libgen.is/search.php?req=${encodeURIComponent(title)}` : null
        }
    };

    // Settings system
    let settings = GM_getValue('settings', {
        SciDB: true,
        AnnasArchive: true,
        SciHub: true,
        LibGen: true
    });

    // Stylish CSS
    GM_addStyle(`
        .access-btns {
            display: flex;
            flex-wrap: wrap; /* Allow buttons to wrap on smaller screens/results */
            gap: 8px;
            margin-top: 10px;
        }
        .access-btn {
            padding: 6px 12px;
            border: none;
            border-radius: 15px;
            color: white !important;
            cursor: pointer;
            font-size: 0.9em;
            text-decoration: none !important;
            text-align: center;
        }
        .settings-panel {
            /* Ensure you have styles for this panel, e.g.: */
            /* position: fixed; top: 20px; right: 20px; background: white; border: 1px solid #ccc; padding: 20px; z-index: 10000; display: none; box-shadow: 0 0 10px rgba(0,0,0,0.1); */
        }
        .settings-icon {
            /* Ensure you have styles for this icon, e.g., for FontAwesome <i class="fas fa-cog"></i> */
            /* position: fixed; top: 25px; right: 25px; font-size: 24px; cursor: pointer; z-index: 10001; color: #555; */
            /* If using innerHTML for icon, you might need to style the <i> tag if FontAwesome isn't available */
        }
    `);

    // Extract DOI
    const extractDOIFromText = (text) => {
        if (!text) return null;

        const doiRegex = /(?:doi:|(?:https?:\/\/)?(?:dx\.)?doi\.org\/)?(10\.\d{4,9}\/(?:[-._;()/:A-Za-z0-9]|%[0-9a-fA-F]{2})+)/i;
        const match = text.match(doiRegex);

        if (match && match[1]) {
            let potentialDoi = match[1];
            potentialDoi = potentialDoi.replace(/%252F/gi, '%2F');
            try {
                potentialDoi = decodeURIComponent(potentialDoi);
            } catch (e) {
                potentialDoi = potentialDoi.replace(/%2F/gi, '/');
            }

            potentialDoi = potentialDoi.split(/[?#]/)[0];
            potentialDoi = potentialDoi.replace(/(\/meta|\/pdf|\.pdf|\/html|\/full|\/fulltext|\/epdf|\/abstract|\/summary|\/xml)$/i, '');

            if (/^10\.\d{4,9}\/.+$/i.test(potentialDoi)) {
                return potentialDoi;
            }
        }
        return null;
    };

    // New comprehensive DOI finder for a Google Scholar result item
    const findDOIForResultItem = (resultItem) => {
        const explicitDoiLink = resultItem.querySelector('a[href*="doi.org/10."], a[href*="dx.doi.org/10."]');
        if (explicitDoiLink && explicitDoiLink.href) {
            const doi = extractDOIFromText(explicitDoiLink.href);
            if (doi) return doi;
        }

        const links = resultItem.querySelectorAll('a[href]');
        for (const link of links) {
            if (link.href) {
                const doi = extractDOIFromText(link.href);
                if (doi) return doi;
            }
        }

        const titleText = resultItem.querySelector('.gs_rt')?.innerText || '';
        const abstractText = resultItem.querySelector('.gs_rs')?.innerText || '';
        const metadataText = resultItem.querySelector('.gs_a')?.innerText || '';

        const combinedText = `${titleText} ${metadataText} ${abstractText}`;
        const doiFromTextContent = extractDOIFromText(combinedText);
        if (doiFromTextContent) return doiFromTextContent;

        const resourceLink = resultItem.querySelector('.gs_ggsd a, .gs_or_ggsm a');
        if (resourceLink && resourceLink.href) {
            const doi = extractDOIFromText(resourceLink.href);
            if (doi) return doi;
        }

        return null;
    };

    // Create settings panel using service keys
    const createSettingsPanel = () => {
        const panel = document.createElement('div');
        panel.className = 'settings-panel';
        Object.assign(panel.style, {
            position: 'fixed', top: '70px', right: '20px', background: 'white',
            border: '1px solid #ccc', padding: '20px', zIndex: '10000',
            display: 'none', boxShadow: '0 0 10px rgba(0,0,0,0.1)'
        });
        panel.innerHTML = `<h3 style="margin: 0 0 15px 0; font-size: 1.2em; color: #333;">Research Access Settings</h3>`;

        Object.entries(services).forEach(([key, service]) => {
            const div = document.createElement('div');
            div.style.margin = '10px 0';
            div.innerHTML = `
                <label style="display: flex; align-items: center; gap: 8px; font-size: 0.95em;">
                    <input type="checkbox" ${settings[key] ? 'checked' : ''}
                           data-service="${key}" style="margin-right: 5px; transform: scale(1.1);">
                    ${service.name}
                </label>
            `;
            panel.appendChild(div);
        });

        document.body.appendChild(panel);
        return panel;
    };

    // Toggle settings panel
    const createSettingsIcon = () => {
        const icon = document.createElement('div');
        icon.className = 'settings-icon';
        Object.assign(icon.style, {
            position: 'fixed', top: '20px', right: '20px', background: '#f0f0f0',
            border: '1px solid #ccc', padding: '8px 10px', cursor: 'pointer',
            zIndex: '10001', borderRadius: '5px', userSelect: 'none', color: '#333'
        });
        icon.innerHTML = '⚙️ Settings';
        icon.addEventListener('click', (e) => {
            e.stopPropagation();
            settingsPanel.style.display = settingsPanel.style.display === 'block' ? 'none' : 'block';
        });
        document.body.appendChild(icon);
        return icon;
    };

    const settingsPanel = createSettingsPanel();
    const settingsIcon = createSettingsIcon();

    // Handle settings changes
    settingsPanel.querySelectorAll('input[type="checkbox"]').forEach(input => {
        input.addEventListener('change', (e) => {
            settings[e.target.dataset.service] = e.target.checked;
            GM_setValue('settings', settings);
            document.querySelectorAll('.access-btns').forEach(btnSet => btnSet.remove());
            addAccessButtons();
        });
    });

    // Main function to add buttons
    const addAccessButtons = () => {
        document.querySelectorAll('.gs_ri').forEach(result => {
            if (result.querySelector('.access-btns')) return;

            const titleElement = result.querySelector('.gs_rt a');
            const title = titleElement ? titleElement.innerText.trim() : (result.querySelector('.gs_rt')?.innerText.trim() || '');
            const doi = findDOIForResultItem(result);

            const btnContainer = document.createElement('div');
            btnContainer.className = 'access-btns';

            Object.entries(services).forEach(([key, service]) => {
                if (!settings[key]) return;

                let identifier;
                if (key === 'SciHub' || key === 'SciDB') {
                    identifier = doi;
                } else {
                    identifier = title;
                }

                if (!identifier) return;

                const url = service.getUrl(identifier);
                if (!url) return;

                const btn = document.createElement('a');
                btn.className = 'access-btn';
                btn.style.backgroundColor = service.color;
                btn.href = url;
                btn.target = '_blank';
                btn.rel = 'noopener noreferrer';
                btn.textContent = service.name;
                btnContainer.appendChild(btn);
            });

            if (btnContainer.hasChildNodes()) {
                result.appendChild(btnContainer);
            }
        });
    };

    let resultsContainer = document.getElementById('gs_res_ccl_mid') || document.getElementById('gs_res_ccl');
    if (!resultsContainer) {
        console.warn("Scholar Access Enhancer: Could not find specific results container. Observing document body.");
        resultsContainer = document.body;
    }

    const observer = new MutationObserver(mutations => {
        let needsButtonUpdate = false;

        for (const mutation of mutations) {
            if (mutation.addedNodes.length > 0) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches && (node.matches('.gs_ri') || node.querySelector('.gs_ri'))) {
                            needsButtonUpdate = true;
                            break;
                        }
                    }
                }
            }
            if (needsButtonUpdate) {
                break;
            }
        }

        if (needsButtonUpdate) {
            addAccessButtons();
        }
    });

    observer.observe(resultsContainer, {
        childList: true,
        subtree: true
    });

    addAccessButtons();

    document.addEventListener('click', (e) => {
        if (settingsPanel.style.display === 'block' && !settingsPanel.contains(e.target) && !settingsIcon.contains(e.target)) {
            settingsPanel.style.display = 'none';
        }
    });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址