Better Papers Cool

Adds cross-links between arXiv.org and papers.cool for easier navigation with advanced date filtering.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Better Papers Cool
// @namespace    http://tampermonkey.net/
// @version      0.2.1
// @description  Adds cross-links between arXiv.org and papers.cool for easier navigation with advanced date filtering.
// @author       SunnyYYLin
// @match        https://arxiv.org/abs/*
// @match        https://papers.cool/arxiv/*
// @grant        GM_xmlhttpRequest
// @connect      arxiv.org
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    /**
     * Configuration for auto-load behavior
     */
    const autoLoadConfig = {
        enabled: false,  // Will be set when filter is applied
        minPapersPerPage: 5,  // Minimum papers to show before auto-loading next page
        maxAutoLoadAttempts: 10,  // Maximum consecutive auto-loads to prevent infinite loops
        currentAttempts: 0
    };

    /**
     * Handles the click event on the [BibTex] button.
     * Fetches BibTeX data from arXiv, copies it to the clipboard, and provides user feedback.
     * @param {Event} event - The click event object.
     * @param {string} arxivId - The arXiv ID of the paper.
     */
    function handleBibtexCopyClick(event, arxivId) {
        event.preventDefault(); // 阻止链接默认跳转

        const bibtexButtonElement = event.currentTarget; // 获取被点击的按钮元素
        const originalText = bibtexButtonElement.textContent;
        const bibtexUrl = `https://arxiv.org/bibtex/${arxivId}`;

        // 改变按钮文字,提供即时反馈
        bibtexButtonElement.textContent = '[Fetching...]';

        // 使用 GM_xmlhttpRequest 执行跨域请求
        GM_xmlhttpRequest({
            method: 'GET',
            url: bibtexUrl,
            onload: function(response) {
                if (response.status >= 200 && response.status < 300) {
                    const bibtexText = response.responseText;
                    navigator.clipboard.writeText(bibtexText).then(() => {
                        bibtexButtonElement.textContent = '[Copied!]';
                        setTimeout(() => {
                            bibtexButtonElement.textContent = originalText;
                        }, 2000);
                    }).catch(err => {
                        console.error('Failed to copy BibTeX: ', err);
                        alert('复制失败,请检查浏览器权限。');
                        bibtexButtonElement.textContent = originalText; // 失败时恢复
                    });
                } else {
                    console.error('Error fetching BibTeX, status:', response.status);
                    alert('获取 BibTeX 数据失败 (服务器状态码: ' + response.status + ')。');
                    bibtexButtonElement.textContent = originalText; // 失败时恢复
                }
            },
            onerror: function(error) {
                console.error('Error fetching BibTeX:', error);
                alert('获取 BibTeX 数据失败 (网络错误)。');
                bibtexButtonElement.textContent = originalText; // 失败时恢复
            }
        });
    }

    /**
     * This function runs on arxiv.org pages.
     * It finds the ArXiv ID and adds a link to the corresponding papers.cool page.
     */
    function enhanceArxivPage() {
        const fullTextDiv = document.querySelector('div.full-text');
        if (!fullTextDiv) {
            console.log('Enhancer Script: Could not find full-text div on arXiv.');
            return;
        }

        let list = fullTextDiv.querySelector('ul');
        if (!list) {
            console.log('Enhancer Script: Could not find link list on arXiv.');
            return;
        }

        const match = window.location.pathname.match(/\/abs\/(.+)/);
        if (!match || !match[1]) {
            console.log('Enhancer Script: Could not parse arXiv ID from URL.');
            return;
        }
        const arxivId = match[1];

        const papersCoolLink = document.createElement('a');
        papersCoolLink.textContent = 'Papers Cool';
        papersCoolLink.href = `https://papers.cool/arxiv/${arxivId}`;
        papersCoolLink.target = '_blank';
        papersCoolLink.rel = 'noopener noreferrer';
        papersCoolLink.className = 'abs-button';
        papersCoolLink.title = 'View on papers.cool';

        const listItem = document.createElement('li');
        listItem.appendChild(papersCoolLink);
        list.appendChild(listItem);
    }

    /**
     * Extracts the publication date from a paper card element.
     * @param {HTMLElement} card - The paper card element.
     * @returns {Date|null} - The parsed date or null if not found.
     */
    function extractPublishDate(card) {
        const dateDataElement = card.querySelector('.date-data');
        if (!dateDataElement) {
            return null;
        }
        
        const dateText = dateDataElement.textContent.trim();
        const date = new Date(dateText);
        
        return isNaN(date.getTime()) ? null : date;
    }

    /**
     * Extracts publish date from HTML string (for pre-filtering)
     * @param {string} htmlString - HTML string containing paper card
     * @returns {Date|null} - The parsed date or null if not found
     */
    function extractPublishDateFromHTML(htmlString) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = htmlString;
        const dateDataElement = tempDiv.querySelector('.date-data');
        
        if (!dateDataElement) {
            return null;
        }
        
        const dateText = dateDataElement.textContent.trim();
        const date = new Date(dateText);
        
        return isNaN(date.getTime()) ? null : date;
    }

    /**
     * Checks if HTML content of a paper matches the filter criteria
     * @param {string} paperHTML - HTML string of a paper card
     * @returns {boolean} - Whether the paper should be shown
     */
    function shouldShowPaperHTML(paperHTML) {
        if (!filterState.isActive) {
            return true;
        }

        const publishDate = extractPublishDateFromHTML(paperHTML);
        
        if (!publishDate) {
            return true; // Show papers without valid dates
        }

        if (filterState.fromDate && publishDate < filterState.fromDate) {
            return false;
        }
        if (filterState.toDate && publishDate > filterState.toDate) {
            return false;
        }

        return true;
    }

    /**
     * Creates and inserts a date filter UI component.
     */
    function createDateFilter() {
        // Check if filter already exists
        if (document.getElementById('date-filter-container')) {
            return;
        }

        // Create filter container
        const filterContainer = document.createElement('div');
        filterContainer.id = 'date-filter-container';
        filterContainer.style.cssText = `
            margin: 20px auto;
            padding: 15px;
            background-color: #f5f5f5;
            border-radius: 8px;
            max-width: 900px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        `;

        // Create filter title
        const filterTitle = document.createElement('h3');
        filterTitle.textContent = '📅 Date Filter';
        filterTitle.style.cssText = `
            margin: 0 0 10px 0;
            font-size: 16px;
            color: #333;
        `;

        // Create date inputs container
        const inputsContainer = document.createElement('div');
        inputsContainer.style.cssText = `
            display: flex;
            gap: 15px;
            align-items: center;
            flex-wrap: wrap;
        `;

        // Create from date input
        const fromLabel = document.createElement('label');
        fromLabel.textContent = 'From: ';
        fromLabel.style.cssText = 'font-weight: bold;';
        
        const fromInput = document.createElement('input');
        fromInput.type = 'date';
        fromInput.id = 'date-filter-from';
        fromInput.style.cssText = `
            padding: 5px 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        `;

        // Create to date input
        const toLabel = document.createElement('label');
        toLabel.textContent = 'To: ';
        toLabel.style.cssText = 'font-weight: bold; margin-left: 10px;';
        
        const toInput = document.createElement('input');
        toInput.type = 'date';
        toInput.id = 'date-filter-to';
        toInput.style.cssText = `
            padding: 5px 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        `;

        // Create apply button
        const applyButton = document.createElement('button');
        applyButton.textContent = 'Apply Filter';
        applyButton.style.cssText = `
            padding: 6px 15px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            margin-left: 10px;
        `;
        applyButton.onmouseover = () => applyButton.style.backgroundColor = '#45a049';
        applyButton.onmouseout = () => applyButton.style.backgroundColor = '#4CAF50';

        // Create reset button
        const resetButton = document.createElement('button');
        resetButton.textContent = 'Reset';
        resetButton.style.cssText = `
            padding: 6px 15px;
            background-color: #f44336;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            margin-left: 5px;
        `;
        resetButton.onmouseover = () => resetButton.style.backgroundColor = '#da190b';
        resetButton.onmouseout = () => resetButton.style.backgroundColor = '#f44336';

        // Create status text
        const statusText = document.createElement('span');
        statusText.id = 'date-filter-status';
        statusText.style.cssText = `
            margin-left: 15px;
            color: #666;
            font-size: 14px;
        `;

        // Create auto-load toggle section
        const autoLoadContainer = document.createElement('div');
        autoLoadContainer.style.cssText = `
            margin-top: 10px;
            padding-top: 10px;
            border-top: 1px solid #ddd;
            display: flex;
            align-items: center;
            gap: 10px;
        `;

        const autoLoadCheckbox = document.createElement('input');
        autoLoadCheckbox.type = 'checkbox';
        autoLoadCheckbox.id = 'auto-load-toggle';
        autoLoadCheckbox.checked = true;
        autoLoadCheckbox.style.cssText = `
            cursor: pointer;
            width: 18px;
            height: 18px;
        `;

        const autoLoadLabel = document.createElement('label');
        autoLoadLabel.htmlFor = 'auto-load-toggle';
        autoLoadLabel.textContent = '🔄 Auto-load more pages when filtered results are too few';
        autoLoadLabel.style.cssText = `
            cursor: pointer;
            font-size: 13px;
            color: #555;
        `;

        const autoLoadInfo = document.createElement('span');
        autoLoadInfo.textContent = '(Min: 5 papers per page)';
        autoLoadInfo.style.cssText = `
            font-size: 12px;
            color: #999;
            margin-left: 5px;
        `;

        autoLoadContainer.appendChild(autoLoadCheckbox);
        autoLoadContainer.appendChild(autoLoadLabel);
        autoLoadContainer.appendChild(autoLoadInfo);

        // Assemble the filter UI
        inputsContainer.appendChild(fromLabel);
        inputsContainer.appendChild(fromInput);
        inputsContainer.appendChild(toLabel);
        inputsContainer.appendChild(toInput);
        inputsContainer.appendChild(applyButton);
        inputsContainer.appendChild(resetButton);
        inputsContainer.appendChild(statusText);

        filterContainer.appendChild(filterTitle);
        filterContainer.appendChild(inputsContainer);
        filterContainer.appendChild(autoLoadContainer);

        // Insert filter before papers container
        const papersContainer = document.querySelector('.papers');
        if (papersContainer) {
            papersContainer.parentNode.insertBefore(filterContainer, papersContainer);
        } else {
            const infoElement = document.querySelector('p.info');
            if (infoElement) {
                infoElement.parentNode.insertBefore(filterContainer, infoElement.nextSibling);
            }
        }

        // Add event listeners
        applyButton.addEventListener('click', applyDateFilter);
        resetButton.addEventListener('click', resetDateFilter);
        
        autoLoadCheckbox.addEventListener('change', (e) => {
            if (e.target.checked && filterState.isActive) {
                // Re-enable auto-load and check if we need more papers
                autoLoadConfig.enabled = true;
                autoLoadConfig.currentAttempts = 0;
                checkAndLoadMorePapers();
            } else {
                autoLoadConfig.enabled = false;
            }
        });
    }

    /**
     * Stores the current filter state for persistence across page updates
     */
    const filterState = {
        fromDate: null,
        toDate: null,
        isActive: false
    };

    /**
     * Checks if a paper should be shown based on current filter state.
     * @param {HTMLElement} card - The paper card element.
     * @returns {boolean} - Whether the paper should be shown.
     */
    function shouldShowPaper(card) {
        if (!filterState.isActive) {
            return true;
        }

        const publishDate = extractPublishDate(card);
        
        if (!publishDate) {
            return true; // Show papers without valid dates
        }

        if (filterState.fromDate && publishDate < filterState.fromDate) {
            return false;
        }
        if (filterState.toDate && publishDate > filterState.toDate) {
            return false;
        }

        return true;
    }

    /**
     * Applies the date filter to show/hide papers based on selected date range.
     * @param {boolean} updateStatus - Whether to update the status text (default: true)
     */
    function applyDateFilter(updateStatus = true) {
        const fromInput = document.getElementById('date-filter-from');
        const toInput = document.getElementById('date-filter-to');
        const statusText = document.getElementById('date-filter-status');

        const fromDate = fromInput.value ? new Date(fromInput.value) : null;
        const toDate = toInput.value ? new Date(toInput.value + 'T23:59:59') : null;

        if (fromDate && toDate && fromDate > toDate) {
            if (statusText) {
                statusText.textContent = '❌ Invalid date range!';
                statusText.style.color = '#f44336';
            }
            return;
        }

        // Update filter state
        filterState.fromDate = fromDate;
        filterState.toDate = toDate;
        filterState.isActive = !!(fromDate || toDate);

        // Enable auto-load when filter is active
        autoLoadConfig.enabled = filterState.isActive;
        autoLoadConfig.currentAttempts = 0;

        const paperCards = document.querySelectorAll('.panel.paper');
        let visibleCount = 0;
        let hiddenCount = 0;

        paperCards.forEach(card => {
            if (shouldShowPaper(card)) {
                card.style.display = '';
                visibleCount++;
            } else {
                card.style.display = 'none';
                hiddenCount++;
            }
        });

        // Update status text
        if (updateStatus && statusText) {
            if (!filterState.isActive) {
                statusText.textContent = '📊 Showing all papers';
                statusText.style.color = '#666';
            } else {
                statusText.textContent = `📊 Showing ${visibleCount} papers (${hiddenCount} filtered out)`;
                statusText.style.color = '#4CAF50';
            }
        }

        // Check if we need to auto-load more papers
        if (filterState.isActive && visibleCount < autoLoadConfig.minPapersPerPage) {
            checkAndLoadMorePapers();
        }
    }

    /**
     * Checks visible paper count and triggers loading more if needed
     */
    function checkAndLoadMorePapers() {
        // Check if auto-load is enabled by user
        const autoLoadCheckbox = document.getElementById('auto-load-toggle');
        const userEnabled = autoLoadCheckbox ? autoLoadCheckbox.checked : true;
        
        if (!userEnabled || !autoLoadConfig.enabled || autoLoadConfig.currentAttempts >= autoLoadConfig.maxAutoLoadAttempts) {
            if (autoLoadConfig.currentAttempts >= autoLoadConfig.maxAutoLoadAttempts) {
                console.log('Date Filter: Reached maximum auto-load attempts.');
                const statusText = document.getElementById('date-filter-status');
                if (statusText) {
                    const currentText = statusText.textContent.replace(' ⚠️ Max pages reached', '');
                    statusText.textContent = currentText + ' ⚠️ Max pages reached';
                }
            }
            return;
        }

        const visiblePapers = document.querySelectorAll('.panel.paper:not([style*="display: none"])');
        
        if (visiblePapers.length < autoLoadConfig.minPapersPerPage) {
            console.log(`Date Filter: Only ${visiblePapers.length} visible papers, attempting to load more...`);
            autoLoadConfig.currentAttempts++;
            
            // Update status to show loading
            const statusText = document.getElementById('date-filter-status');
            if (statusText) {
                statusText.textContent += ` 🔄 Loading page ${autoLoadConfig.currentAttempts}...`;
            }
            
            // Try to trigger the pagination plugin
            triggerNextPage();
        }
    }

    /**
     * Attempts to trigger the next page load by simulating scroll or clicking next button
     */
    function triggerNextPage() {
        // Method 1: Try to find and click the "next" button or pagination link
        const nextButton = document.querySelector('a[rel="next"], .pagination .next, button.load-more');
        if (nextButton && !nextButton.disabled) {
            console.log('Date Filter: Clicking next page button...');
            setTimeout(() => nextButton.click(), 500);
            return;
        }

        // Method 2: Trigger scroll event (for infinite scroll plugins)
        console.log('Date Filter: Triggering scroll to load more papers...');
        setTimeout(() => {
            window.scrollTo({
                top: document.body.scrollHeight,
                behavior: 'smooth'
            });
            
            // Also dispatch scroll event
            window.dispatchEvent(new Event('scroll'));
            
            // Check again after a delay
            setTimeout(() => {
                checkAndLoadMorePapers();
            }, 2000);
        }, 500);
    }

    /**
     * Resets the date filter and shows all papers.
     */
    function resetDateFilter() {
        const fromInput = document.getElementById('date-filter-from');
        const toInput = document.getElementById('date-filter-to');
        const statusText = document.getElementById('date-filter-status');

        fromInput.value = '';
        toInput.value = '';
        if (statusText) {
            statusText.textContent = '';
        }

        // Clear filter state
        filterState.fromDate = null;
        filterState.toDate = null;
        filterState.isActive = false;

        // Disable auto-load
        autoLoadConfig.enabled = false;
        autoLoadConfig.currentAttempts = 0;

        const paperCards = document.querySelectorAll('.panel.paper');
        paperCards.forEach(card => {
            card.style.display = '';
        });
    }

    /**
     * Sets up a MutationObserver to watch for new papers being added to the page.
     * This ensures that auto-pagination plugins work with the date filter.
     */
    function setupPaperObserver() {
        const papersContainer = document.querySelector('.papers');
        if (!papersContainer) {
            console.log('Date Filter: Could not find papers container for observation.');
            return;
        }

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

            mutations.forEach((mutation) => {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach((node) => {
                        // Check if the added node is a paper card
                        if (node.nodeType === 1 && node.classList && node.classList.contains('paper')) {
                            newPapersAdded = true;
                            
                            // Apply filter to the new paper immediately
                            if (filterState.isActive) {
                                if (!shouldShowPaper(node)) {
                                    node.style.display = 'none';
                                }
                            }

                            // Also add arXiv and BibTeX links to new papers
                            const arxivId = node.id;
                            if (arxivId) {
                                addArxivAndBibtexLinks(node, arxivId);
                            }
                        }
                    });
                }
            });

            // Update status if new papers were added and filter is active
            if (newPapersAdded && filterState.isActive) {
                // Use a slight delay to ensure all papers are processed
                setTimeout(() => {
                    const allPapers = document.querySelectorAll('.panel.paper');
                    let visibleCount = 0;
                    let hiddenCount = 0;

                    allPapers.forEach(card => {
                        if (card.style.display === 'none') {
                            hiddenCount++;
                        } else {
                            visibleCount++;
                        }
                    });

                    const statusText = document.getElementById('date-filter-status');
                    if (statusText) {
                        statusText.textContent = `📊 Showing ${visibleCount} papers (${hiddenCount} filtered out)`;
                        statusText.style.color = '#4CAF50';
                    }

                    // Check if we need to load more papers
                    if (filterState.isActive && visibleCount < autoLoadConfig.minPapersPerPage) {
                        setTimeout(() => checkAndLoadMorePapers(), 1000);
                    } else {
                        // Reset attempt counter if we have enough papers
                        autoLoadConfig.currentAttempts = 0;
                    }
                }, 100);
            }
        });

        // Start observing the papers container for changes
        observer.observe(papersContainer, {
            childList: true,
            subtree: true
        });

        console.log('Date Filter: MutationObserver set up to watch for new papers.');
    }

    /**
     * Helper function to add arXiv and BibTeX links to a paper card.
     * Extracted to avoid code duplication for dynamically added papers.
     * @param {HTMLElement} card - The paper card element.
     * @param {string} arxivId - The arXiv ID of the paper.
     */
    function addArxivAndBibtexLinks(card, arxivId) {
        const titleHeader = card.querySelector('h2.title');
        if (!titleHeader) {
            return;
        }

        // Ensure [arXiv] button exists (don't confuse with other arXiv links like the index anchor)
        let hasArxivButton = false;
        titleHeader.querySelectorAll('a').forEach(a => {
            if (/\[arXiv\]/i.test((a.textContent || '').trim())) {
                hasArxivButton = true;
            }
        });
        if (!hasArxivButton && arxivId) {
            const arxivButton = document.createElement('a');
            arxivButton.textContent = '[arXiv]';
            arxivButton.href = `https://arxiv.org/abs/${arxivId}`;
            arxivButton.target = '_blank';
            arxivButton.title = 'View on arXiv';
            arxivButton.className = 'title-rel notranslate';
            arxivButton.style.marginLeft = '3px';
            titleHeader.append(' ', arxivButton);
        }

        // Ensure [BibTex] link exists
        let hasBibtex = false;
        titleHeader.querySelectorAll('a').forEach(a => {
            if (/bibtex/i.test(a.textContent || '')) {
                hasBibtex = true;
            }
        });
        if (!hasBibtex && arxivId) {
            const bibtexButton = document.createElement('a');
            bibtexButton.textContent = '[BibTex]';
            bibtexButton.href = '#';
            bibtexButton.title = 'Copy BibTeX citation';
            bibtexButton.className = 'title-rel notranslate';
            bibtexButton.style.marginLeft = '3px';
            bibtexButton.addEventListener('click', (event) => {
                handleBibtexCopyClick(event, arxivId);
            });
            titleHeader.append(' ', bibtexButton);
        }
    }

    /**
     * This function runs on papers.cool pages.
     * It finds the link back to ArXiv and adds new links for BibTeX and direct PDF access.
     */
    function enhancePapersCoolPage() {
        const paperCards = document.querySelectorAll('.panel.paper');

        paperCards.forEach(card => {
            const arxivId = card.id;
            if (!arxivId) {
                return;
            }

            addArxivAndBibtexLinks(card, arxivId);
        });

        // Create date filter UI
        createDateFilter();

        // Set up observer for dynamically added papers (auto-pagination compatibility)
        setupPaperObserver();
    }

    /**
     * Main execution block.
     */
    function run() {
        const hostname = window.location.hostname;

        if (hostname.includes('arxiv.org')) {
            enhanceArxivPage();
            console.log('Enhancer Script: Running on arXiv.org');
        } else if (hostname.includes('papers.cool')) {
            enhancePapersCoolPage();
            console.log('Enhancer Script: Running on papers.cool');
        } else {
            console.log('Enhancer Script: Not on arXiv.org or papers.cool, no action taken.');
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', run);
    } else {
        run();
    }
})();