Better Papers Cool

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

// ==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();
    }
})();