📚 The GOD MODE Ultimate Research Superpanel for Scholars, Hackers & Knowledge Addicts

GOD MODE Instantly unlock academic papers, PDFs, and DOIs from everywhere — LibGen, Sci-Hub, Unpaywall, Semantic Scholar, Crossref, and more — directly inside any webpage.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         📚 The GOD MODE Ultimate Research Superpanel for Scholars, Hackers & Knowledge Addicts
// @namespace    http://instagram.com/WaterDustLab
// @version      4.0
// @description  GOD MODE Instantly unlock academic papers, PDFs, and DOIs from everywhere — LibGen, Sci-Hub, Unpaywall, Semantic Scholar, Crossref, and more — directly inside any webpage.
// @author       WaterDustLab
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      *
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ---------- Config / Keys ----------
    const PANEL_ID = 'v13-panel';
    const REOPEN_ID = 'v13-reopen';
    const PAGE_KEY = 'v13_panel_closed_' + btoa(location.href.split('?')[0]).substring(0, 50);
    const TOGGLES_KEY = 'v13_toggles_v2';
    const SETTINGS_KEY = 'v13_settings_v2';
    const OPENED_KEY_PREFIX = 'v13_opened_for_';
    const CACHE_KEY = 'v13_cache_v2';
    const DIM_KEY = 'v13_dims_v1';
    const THEME_KEY = 'v13_theme_v1'; // 'auto' | 'dark' | 'light'
    const CACHE_TTL = 60 * 60 * 1000;

    // mirror lists
    const proxies = {
        sciHub: ['https://sci-hub.st', 'https://sci-hub.ru', 'https://sci-hub.se', 'https://sci-hub.do'],
        libGen: ['https://libgen.is', 'https://libgen.gs', 'https://libgen.li', 'https://libgen.me', 'http://gen.lib.rus.ec'],
        annas: ['https://annas-archive.org', 'https://annas-archive.se', 'https://annas-archive.li'],
        zLib: ['https://z-lib.id', 'https://z-library.sk', 'https://z-lib.fm', 'https://z-lib.gs', 'https://1lib.sk']
    };

    // sources
    const sources = [
        { id: 'Unpaywall', name: 'Unpaywall', color: '#52b788', helper: 'unpaywall' },
        { id: 'LibGen', name: 'LibGen', color: '#ff6b6b', proxy: 'libGen', prefill: true },
        { id: 'Annas', name: 'Annas', color: '#4ecdc4', proxy: 'annas', prefill: true },
        { id: 'CORE', name: 'CORE', color: '#45b7d1', url: (doi) => `https://core.ac.uk/search?q=${encodeURIComponent(doi)}` },
        { id: 'OAButton', name: 'OA Button', color: '#96ceb4', url: (doi) => `https://openaccessbutton.org/api/instant?doi=${encodeURIComponent(doi)}` },
        { id: 'ResearchGate', name: 'ResearchGate', color: '#00bfff', url: (doi) => `https://www.researchgate.net/search/publication?q=${encodeURIComponent(doi)}`, prefill: true },
        { id: 'ZLib', name: 'Z-Library', color: '#dda0dd', proxy: 'zLib', prefill: true },
        { id: 'OAMG', name: 'OA.mg', color: '#98d8c8', url: (doi) => `https://oa.mg/search?q=${encodeURIComponent(doi)}`, prefill: true }
    ];

    // ---------- Safe storage wrappers ----------
    function safeGet(key, def) {
        try { return GM_getValue(key, def); } catch (e) {
            try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : def; } catch { return def; }
        }
    }
    function safeSet(key, val) {
        try { GM_setValue(key, val); } catch (e) {
            try { localStorage.setItem(key, JSON.stringify(val)); } catch {}
        }
    }

    const getToggles = () => safeGet(TOGGLES_KEY, {});
    const setToggles = (o) => safeSet(TOGGLES_KEY, o);
    const getSettings = () => safeGet(SETTINGS_KEY, { minimal: false, showAll: true, selected: [] });
    const setSettings = (o) => safeSet(SETTINGS_KEY, o);
    const cacheGet = () => safeGet(CACHE_KEY, {});
    const cacheSet = (o) => safeSet(CACHE_KEY, o);
    const getDims = () => safeGet(DIM_KEY, { width: 360, fontSize: 14 });
    const setDims = (o) => safeSet(DIM_KEY, o);
    const getThemePref = () => safeGet(THEME_KEY, 'auto');
    const setThemePref = (v) => safeSet(THEME_KEY, v);

    const log = (...args) => console.log('[AA-v13]', ...args);

    // ---------- DOI helpers ----------
    function getDOI() {
        const m = location.href.match(/10\.\d{4,9}\/[^\s?#&"']+/i);
        if (m) return m[0];
        const meta = document.querySelector('meta[name="citation_doi"], meta[property="doi"]');
        if (meta) return meta.content && meta.content.trim();
        const bodyMatch = document.body && document.body.innerText.match(/10\.\d{4,9}\/[^\s.,;(){}[\]]{5,}/i);
        return bodyMatch ? bodyMatch[0] : null;
    }
    function isMainDOI() {
        return !/references|\/citation\//i.test(location.href);
    }

    // ---------- Theme helpers ----------
    function systemPrefersDark() {
        return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
    }
    function resolveTheme() {
        const pref = getThemePref(); // 'auto'|'dark'|'light'
        if (pref === 'dark') return 'dark';
        if (pref === 'light') return 'light';
        return systemPrefersDark() ? 'dark' : 'light';
    }
    function applyTheme(panel) {
        const theme = resolveTheme();
        if (!panel) panel = document.getElementById(PANEL_ID);
        if (!panel) return;
        if (theme === 'dark') {
            panel.style.background = 'rgba(20,20,30,0.95)';
            panel.style.color = '#fff';
        } else {
            panel.style.background = '#fff';
            panel.style.color = '#111';
        }
        // update other UI bits (settings modal will read getThemePref for preview)
    }

    // ---------- Reopen / toggle button (📚) ----------
    function createReopenButton() {
        if (document.getElementById(REOPEN_ID)) return;
        const btn = document.createElement('div');
        btn.id = REOPEN_ID;
        btn.title = 'Toggle Paper Panel';
        btn.innerText = '📚';
        Object.assign(btn.style, {
            position: 'fixed',
            right: '18px',
            bottom: '18px',
            zIndex: 999999,
            width: '44px',
            height: '44px',
            borderRadius: '50%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            background: '#079992',
            color: '#fff',
            cursor: 'pointer',
            boxShadow: '0 6px 18px rgba(0,0,0,0.35)'
        });
        btn.onclick = () => {
            const panel = document.getElementById(PANEL_ID);
            if (panel) { panel.remove(); return; }
            buildPanel();
        };
        document.body.appendChild(btn);
    }

    // ---------- Panel builder ----------
    async function buildPanel() {
        const doi = getDOI();
        if (!doi || !isMainDOI()) { createReopenButton(); return; }

        const existing = document.getElementById(PANEL_ID);
        if (existing) existing.remove();

        const dims = getDims();
        const panel = document.createElement('div');
        panel.id = PANEL_ID;
        panel.setAttribute('role', 'dialog');
        panel.style.position = 'fixed';
        panel.style.top = '12px';
        panel.style.right = '12px';
        panel.style.zIndex = 9999999;
        panel.style.padding = '12px';
        panel.style.borderRadius = '12px';
        panel.style.fontFamily = 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial';
        panel.style.boxShadow = '0 8px 28px rgba(0,0,0,0.6)';
        panel.style.maxWidth = '90%';
        panel.style.width = `${Math.max(260, Math.min(600, dims.width))}px`;
        panel.style.fontSize = `${Math.max(12, Math.min(20, dims.fontSize))}px`;
        panel.style.touchAction = 'none'; // help with touch drag
        applyTheme(panel);

        // header + status
        const header = document.createElement('div');
        header.style.display = 'flex';
        header.style.alignItems = 'center';
        header.style.gap = '8px';
        const title = document.createElement('div'); title.textContent = 'v13: Source Panel'; title.style.fontWeight = '700';
        const status = document.createElement('div'); status.style.marginLeft = '8px'; status.style.fontSize = '12px'; status.textContent = 'Checking...';
        header.appendChild(title); header.appendChild(status);

        const close = document.createElement('div');
        close.innerText = '×'; close.title = 'Close panel (reopen via 📚)';
        Object.assign(close.style, { marginLeft: 'auto', cursor: 'pointer', fontSize: '18px' });
        close.onclick = () => { safeSet(PAGE_KEY, true); panel.remove(); createReopenButton(); };
        header.appendChild(close);
        panel.appendChild(header);

        // settings button separate row (below header)
        const settingsBtn = document.createElement('button');
        settingsBtn.textContent = '⚙'; settingsBtn.title = 'Settings';
        Object.assign(settingsBtn.style, { cursor: 'pointer' });
        settingsBtn.onclick = () => showSettings(panel);
        const settingsRow = document.createElement('div');
        settingsRow.style.textAlign = 'right';
        settingsRow.style.marginTop = '6px';
        settingsRow.appendChild(settingsBtn);
        panel.appendChild(settingsRow);

        // main container grid
        const container = document.createElement('div');
        container.style.marginTop = '10px';
        container.style.display = 'grid';
        container.style.gridTemplateColumns = '1fr 40px';
        container.style.gap = '8px';

        // decide which sources to show
        const settings = getSettings();
        let toShow = sources.slice();
        if (settings.minimal && settings.selected?.length) toShow = sources.filter(s => settings.selected.includes(s.id));
        else if (!settings.showAll) toShow = sources.filter(s => ['Unpaywall', 'LibGen', 'Annas'].includes(s.id));

        // Sci-Hub mirrors + checkboxes (grid: label + checkbox)
        const sciHubBox = document.createElement('div');
        sciHubBox.style.gridColumn = '1 / span 2';
        sciHubBox.style.display = 'grid';
        sciHubBox.style.gridTemplateColumns = '1fr 40px';
        sciHubBox.style.gap = '6px';
        proxies.sciHub.forEach(m => {
            const label = m.replace(/^https?:\/\//, '');
            const id = `SciHub-${label}`;
            const btn = createSourceBtn(label, '#e74c3c', id, doi, { mirror: m, isSciHub: true });
            const chkWrap = document.createElement('div');
            const chk = document.createElement('input');
            chk.type = 'checkbox';
            chk.dataset.srcidchk = id;
            const t = getToggles();
            chk.checked = !!t[id];
            chk.onclick = () => { const tg = getToggles(); tg[id] = chk.checked; setToggles(tg); };
            chk.style.width = '18px'; chk.style.height = '18px';
            chkWrap.appendChild(chk);
            sciHubBox.appendChild(btn.wrapper);
            sciHubBox.appendChild(chkWrap);
        });
        container.appendChild(sciHubBox);

        // other sources
        toShow.forEach(src => {
            const btn = createSourceBtn(src.name, src.color, src.id, doi, src);
            container.appendChild(btn.wrapper);
            container.appendChild(btn.checkboxWrapper);
        });

        panel.appendChild(container);

        // resizer: right-edge vertical handle (touch-friendly)
        const resizer = document.createElement('div');
        resizer.id = 'v13-resizer';
        // small visible handle on the right edge
        Object.assign(resizer.style, {
            position: 'absolute',
            top: '6px',
            bottom: '6px',
            right: '-10px', // slightly outside panel
            width: '20px',
            borderRadius: '10px',
            zIndex: 10000001,
            cursor: 'ew-resize',
            touchAction: 'none',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            // make subtle semi-transparent handle so user can find it
            background: 'transparent'
        });
        // inner visible bar
        const bar = document.createElement('div');
        Object.assign(bar.style, {
            width: '4px',
            height: '40px',
            borderRadius: '4px',
            background: 'rgba(0,0,0,0.15)'
        });
        resizer.appendChild(bar);
        panel.appendChild(resizer);

        // apply theme & restore dims
        applyTheme(panel);

        // add to document
        document.body.appendChild(panel);
        createReopenButton();

        // run checks
        status.textContent = 'Running checks...';
        const buttons = Array.from(panel.querySelectorAll('[data-srcid]'));
        const cache = cacheGet();
        let checked = 0;
        for (const el of buttons) {
            const id = el.dataset.srcid;
            const meta = JSON.parse(el.dataset.meta || '{}');
            const result = await checkSourceByMeta(id, doi, meta, cache);
            updateButtonState(el, result);
            checked++;
            status.textContent = `Checked ${checked}/${buttons.length}`;
        }
        cacheSet(cache);

        // auto-open checked (avoid duplicates)
        const toggles = getToggles();
        const openedKey = OPENED_KEY_PREFIX + doi;
        const alreadyOpened = safeGet(openedKey, false);
        if (!alreadyOpened) {
            for (const el of buttons) {
                const id = el.dataset.srcid;
                const meta = JSON.parse(el.dataset.meta || '{}');
                const chk = panel.querySelector(`input[data-srcidchk="${id}"]`);
                if (chk && chk.checked) {
                    const url = buildUrlForMeta(id, doi, meta);
                    if (!url) continue;
                    const openedTabs = toggles._openedTabs || {};
                    if (!openedTabs[url]) {
                        GM_openInTab(url, { active: false });
                        log('Opened background tab for', id, url);
                        openedTabs[url] = true;
                        toggles._openedTabs = openedTabs;
                        setToggles(toggles);
                    }
                }
            }
            safeSet(openedKey, true);
        }

        // ---------- Resizer behavior (touch + mouse)
        (function attachResizer() {
            const minW = 260, maxW = 600;
            const minFont = 12, maxFont = 20;
            let dragging = false;
            let startX = 0;
            let startW = 0;

            function onStart(clientX) {
                dragging = true;
                startX = clientX;
                startW = panel.getBoundingClientRect().width;
                document.body.style.userSelect = 'none';
                document.body.style.touchAction = 'none';
            }
            function onMove(clientX) {
                if (!dragging) return;
                const dx = startX - clientX; // moving left increases width? We'll invert
                // we want dragging to the LEFT to increase width (since handle on right outside),
                // but users on mobile will drag left/right; use delta accordingly:
                const newW = Math.round(Math.max(minW, Math.min(maxW, startW + (startX - clientX))));
                panel.style.width = newW + 'px';
                // proportional font mapping
                const ratio = (newW - minW) / (maxW - minW);
                const newFont = Math.round(minFont + ratio * (maxFont - minFont));
                panel.style.fontSize = newFont + 'px';
            }
            function onEnd() {
                if (!dragging) return;
                dragging = false;
                document.body.style.userSelect = '';
                document.body.style.touchAction = '';
                // persist dims
                const finalW = parseInt(panel.style.width, 10) || getDims().width;
                const finalFont = parseInt(panel.style.fontSize, 10) || getDims().fontSize;
                setDims({ width: finalW, fontSize: finalFont });
            }

            // touch events
            resizer.addEventListener('touchstart', (e) => {
                if (e.touches && e.touches[0]) onStart(e.touches[0].clientX);
            }, { passive: true });
            window.addEventListener('touchmove', (e) => {
                if (e.touches && e.touches[0]) { onMove(e.touches[0].clientX); }
            }, { passive: true });
            window.addEventListener('touchend', onEnd);

            // mouse events (desktop)
            resizer.addEventListener('mousedown', (e) => { e.preventDefault(); onStart(e.clientX); });
            window.addEventListener('mousemove', (e) => onMove(e.clientX));
            window.addEventListener('mouseup', onEnd);
        })();

        // expose panel to settings so theme preview can be applied
        panel.dataset.v13 = '1';
    }

    // ---------- Create per-source button and checkbox ----------
    function createSourceBtn(label, color, id, doi, meta = {}) {
        const wrapper = document.createElement('div');
        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'center';
        wrapper.style.gap = '6px';

        const a = document.createElement('a');
        a.href = 'javascript:void(0)';
        a.textContent = label;
        a.dataset.srcid = id;
        a.dataset.meta = JSON.stringify(meta);
        Object.assign(a.style, { background: color, color: '#000', padding: '8px 10px', borderRadius: '10px', textDecoration: 'none', fontWeight: '700', display: 'inline-block', minWidth: '120px', textAlign: 'center' });

        const icon = document.createElement('span');
        icon.className = 'v13-icon';
        icon.textContent = '⌛';
        icon.style.marginRight = '6px';
        a.prepend(icon);

        a.onclick = async (e) => {
            e.preventDefault();
            const doiNow = getDOI();
            const url = buildUrlForMeta(id, doiNow, meta);
            if (id === 'Unpaywall') {
                GM_openInTab(url || 'https://unpaywall.org/', { active: true });
                GM_openInTab(`https://web.archive.org/web/*/${encodeURIComponent(doiNow)}`, { active: false });
                GM_openInTab(`https://scholar.google.com/scholar?q=${encodeURIComponent(doiNow)}`, { active: false });
                return;
            }
            if (url) GM_openInTab(url, { active: true });
        };

        const checkboxWrapper = document.createElement('div');
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.dataset.srcidchk = id;
        const t = getToggles();
        checkbox.checked = !!t[id];
        checkbox.onclick = () => { const tg = getToggles(); tg[id] = checkbox.checked; setToggles(tg); };
        checkbox.style.width = '18px'; checkbox.style.height = '18px';
        checkboxWrapper.appendChild(checkbox);

        wrapper.appendChild(a);
        return { wrapper, checkboxWrapper, linkElem: a, checkbox };
    }

    // ---------- URL builder ----------
    function buildUrlForMeta(id, doi, meta) {
        if (!doi) return null;
        if (id.startsWith('SciHub-')) return `${meta.mirror}/search?request=${encodeURIComponent(doi)}`;
        if (id === 'Unpaywall') return `https://unpaywall.org/${encodeURIComponent(doi)}`;
        if (id === 'LibGen') {
            const live = getLiveMirrorSync('libGen') || proxies.libGen[0];
            return `${live}/search.php?req=${encodeURIComponent(doi)}`;
        }
        if (id === 'Annas') {
            const live = getLiveMirrorSync('annas') || proxies.annas[0];
            return `${live}/search?q=${encodeURIComponent(doi)}`;
        }
        if (id === 'ZLib') {
            const live = getLiveMirrorSync('zLib') || proxies.zLib[0];
            return `${live}/search.php?req=${encodeURIComponent(doi)}`;
        }
        if (meta && meta.url) return meta.url(doi);
        return `https://www.google.com/search?q=${encodeURIComponent(doi)}`;
    }

    // ---------- Mirror helpers ----------
    function getLiveMirrorSync(type) {
        const cache = cacheGet();
        const key = `live_${type}`;
        if (cache[key] && (Date.now() - cache[key].ts) < CACHE_TTL) return cache[key].mirror;
        return (proxies[type] && proxies[type][0]) || null;
    }

    async function checkSourceByMeta(id, doi, meta, cache) {
        const key = `${doi}_${id}`;
        const now = Date.now();
        if (cache[key] && (now - cache[key].ts) < CACHE_TTL) return cache[key].status;
        let url = buildUrlForMeta(id, doi, meta);
        if (!url) { cache[key] = { status: 'fail', ts: Date.now(), url: null }; cacheSet(cache); return 'fail'; }
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                timeout: 5000,
                onload: (r) => {
                    const txt = (r.responseText || '').toLowerCase();
                    let status = 'fail';
                    if ((r.finalUrl && r.finalUrl.endsWith('.pdf')) || (r.responseHeaders && /application\/pdf/i.test(r.responseHeaders))) status = 'success';
                    else if (r.status === 200 && /pdf|download|doi/.test(txt)) status = 'partial';
                    cache[key] = { status, ts: Date.now(), url };
                    cacheSet(cache);
                    resolve(status);
                },
                onerror: () => { cache[key] = { status: 'fail', ts: Date.now(), url }; cacheSet(cache); resolve('fail'); },
                ontimeout: () => { cache[key] = { status: 'fail', ts: Date.now(), url }; cacheSet(cache); resolve('fail'); }
            });
        });
    }

    // ---------- Update button visuals ----------
    function updateButtonState(aElement, status) {
        const icon = aElement.querySelector('.v13-icon') || aElement;
        if (!icon) return;
        if (status === 'success') { icon.textContent = '✓'; icon.style.color = '#2ecc71'; }
        else if (status === 'partial') { icon.textContent = '⚠'; icon.style.color = '#f39c12'; }
        else { icon.textContent = '✗'; icon.style.color = '#e74c3c'; }
    }

    // ---------- Settings UI ----------
    function showSettings(panelRef) {
        // if exists remove
        const existing = document.getElementById('v13-settings');
        if (existing) return existing.remove();

        const s = getSettings();
        const dims = getDims();
        const themePref = getThemePref();

        const box = document.createElement('div');
        box.id = 'v13-settings';
        Object.assign(box.style, {
            position: 'fixed',
            left: '50%',
            top: '50%',
            transform: 'translate(-50%,-50%)',
            zIndex: 10000002,
            background: resolveTheme() === 'dark' ? '#111' : '#fff',
            color: resolveTheme() === 'dark' ? '#fff' : '#111',
            padding: '14px',
            borderRadius: '10px',
            boxShadow: '0 10px 40px rgba(0,0,0,0.6)',
            maxHeight: '80vh',
            overflowY: 'auto',
            minWidth: '260px'
        });

        const h = document.createElement('div');
        h.style.fontWeight = '700';
        h.style.marginBottom = '8px';
        h.textContent = 'Panel Settings';
        box.appendChild(h);

        // Show all toggle
        const showAllLabel = document.createElement('label');
        showAllLabel.innerHTML = `<input type="checkbox" id="v13-showall"> Show All Sources`;
        box.appendChild(showAllLabel);
        box.appendChild(document.createElement('br'));

        // Minimal mode master checkbox
        const minimalLabel = document.createElement('label');
        minimalLabel.style.display = 'block';
        minimalLabel.style.marginTop = '8px';
        minimalLabel.innerHTML = `<input type="checkbox" id="v13-minimal"> Use Minimal Mode (show only selected below)`;
        box.appendChild(minimalLabel);

        // Manual minimal selection list
        const srcTitle = document.createElement('div');
        srcTitle.style.margin = '8px 0 6px 0';
        srcTitle.textContent = 'Select sources for Minimal Mode:';
        box.appendChild(srcTitle);

        sources.forEach(src => {
            const id = `v13-src-${src.id}`;
            const line = document.createElement('label');
            line.style.display = 'block';
            line.innerHTML = `<input type="checkbox" id="${id}"> ${src.name}`;
            box.appendChild(line);
        });

        // Sci-Hub mirror checkboxes (also exposed here)
        const mirrorsTitle = document.createElement('div');
        mirrorsTitle.style.margin = '8px 0 6px 0';
        mirrorsTitle.textContent = 'Sci-Hub Mirrors (also used for background opens):';
        box.appendChild(mirrorsTitle);
        proxies.sciHub.forEach(m => {
            const label = m.replace(/^https?:\/\//, '');
            const id = `v13-mirror-${label}`;
            const line = document.createElement('label');
            line.style.display = 'block';
            line.innerHTML = `<input type="checkbox" id="${id}"> ${label}`;
            box.appendChild(line);
        });

        // Theme controls (auto/dark/light)
        const themeTitle = document.createElement('div');
        themeTitle.style.margin = '8px 0 6px 0';
        themeTitle.textContent = 'Theme:';
        box.appendChild(themeTitle);

        const themeRow = document.createElement('div');
        ['auto','dark','light'].forEach(opt => {
            const r = document.createElement('label');
            r.style.marginRight = '8px';
            r.innerHTML = `<input type="radio" name="v13-theme" value="${opt}"> ${opt}`;
            themeRow.appendChild(r);
        });
        box.appendChild(themeRow);

        // Explicit dims controls (sliders for accessibility)
        const dimsTitle = document.createElement('div');
        dimsTitle.style.margin = '8px 0 6px 0';
        dimsTitle.textContent = 'Width & Font size (also adjustable with right-edge handle):';
        box.appendChild(dimsTitle);

        const widthLabel = document.createElement('div');
        widthLabel.textContent = `Width: ${dims.width}px`;
        box.appendChild(widthLabel);
        const widthRange = document.createElement('input');
        widthRange.type = 'range';
        widthRange.min = 260; widthRange.max = 600; widthRange.value = dims.width;
        widthRange.style.width = '100%';
        box.appendChild(widthRange);

        const fontLabel = document.createElement('div');
        fontLabel.textContent = `Font: ${dims.fontSize}px`;
        box.appendChild(fontLabel);
        const fontRange = document.createElement('input');
        fontRange.type = 'range';
        fontRange.min = 12; fontRange.max = 20; fontRange.value = dims.fontSize;
        fontRange.style.width = '100%';
        box.appendChild(fontRange);

        const hr = document.createElement('hr');
        hr.style.margin = '10px 0';
        box.appendChild(hr);

        // buttons
        const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save';
        const closeBtn = document.createElement('button'); closeBtn.textContent = 'Close';
        closeBtn.style.marginLeft = '8px';
        box.appendChild(saveBtn); box.appendChild(closeBtn);

        // inject into doc
        document.body.appendChild(box);

        // initialize UI states
        document.getElementById('v13-showall').checked = s.showAll;
        document.getElementById('v13-minimal').checked = s.minimal;
        if (s.selected?.length) s.selected.forEach(id => {
            const el = document.getElementById(`v13-src-${id}`);
            if (el) el.checked = true;
        });
        // mirrors toggles
        const toggles = getToggles();
        proxies.sciHub.forEach(m => {
            const label = m.replace(/^https?:\/\//, '');
            const el = document.getElementById(`v13-mirror-${label}`);
            if (el) el.checked = !!toggles[`SciHub-${label}`];
        });
        // theme
        const themeInputs = box.querySelectorAll('input[name="v13-theme"]');
        themeInputs.forEach(r => { if (r.value === themePref) r.checked = true; });
        // dims
        widthRange.oninput = () => { widthLabel.textContent = `Width: ${widthRange.value}px`; };
        fontRange.oninput = () => { fontLabel.textContent = `Font: ${fontRange.value}px`; };

        // handlers
        saveBtn.onclick = () => {
            // collect selections
            const newSettings = { showAll: document.getElementById('v13-showall').checked, minimal: document.getElementById('v13-minimal').checked, selected: [] };
            sources.forEach(src => { const el = document.getElementById(`v13-src-${src.id}`); if (el && el.checked) newSettings.selected.push(src.id); });
            setSettings(newSettings);
            // mirrors propagate to toggles
            const newToggles = getToggles();
            proxies.sciHub.forEach(m => {
                const label = m.replace(/^https?:\/\//, '');
                const el = document.getElementById(`v13-mirror-${label}`);
                if (el) newToggles[`SciHub-${label}`] = !!el.checked;
            });
            setToggles(newToggles);
            // theme
            const chosen = box.querySelector('input[name="v13-theme"]:checked')?.value || 'auto';
            setThemePref(chosen);
            // dims
            const chosenW = parseInt(widthRange.value, 10);
            const chosenF = parseInt(fontRange.value, 10);
            setDims({ width: chosenW, fontSize: chosenF });
            // if panel open, apply dims & theme
            const panel = document.getElementById(PANEL_ID);
            if (panel) {
                panel.style.width = `${chosenW}px`;
                panel.style.fontSize = `${chosenF}px`;
                applyTheme(panel);
            }
            box.remove();
        };
        closeBtn.onclick = () => box.remove();
    }

    // ---------- Init on load ----------
    window.addEventListener('load', () => {
        createReopenButton();
        // build automatically unless closed via PAGE_KEY
        try {
            const closed = safeGet(PAGE_KEY, false);
            if (!closed) buildPanel();
        } catch {
            buildPanel();
        }

        // watch for system theme changes (if user set auto)
        if (window.matchMedia) {
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener?.('change', e => {
                if (getThemePref() === 'auto') {
                    const panel = document.getElementById(PANEL_ID);
                    if (panel) applyTheme(panel);
                }
            });
        }
    });

})();