Greasy Fork

Greasy Fork is available in English.

长佩章节下载器

从长佩(gongzicp.com)下载章节文本

// ==UserScript==
// @name                Changpei Chapter Downloader
// @name:zh-CN          长佩章节下载器
// @namespace           http://tampermonkey.net/
// @version             0.2
// @description         Download chapter content from Changpei(gongzicp.com)
// @description:zh-CN   从长佩(gongzicp.com)下载章节文本
// @author              oovz
// @match               *://*gongzicp.com/read-*.html
// @grant               none
// @source              https://gist.github.com/oovz/8c1c38607ed01cb594ebbd4913ff2c60
// @source              http://greasyfork.icu/en/scripts/536172-changpei-chapter-downloader
// @license             MIT
// ==/UserScript==

(function() {
    'use strict';    
    // Configure your selectors here
    const APP_WRAPPER_SELECTOR = '#app'; // for MutationObserver
    const TITLE_SELECTOR = 'div.title > div.name'; // for title
    const CONTENT_SELECTOR = 'div.h-reader > div.content'; // for content
    const NEXT_CHAPTER_BASE_SELECTOR = 'div#readPage div.item > a'; // for next chaper link
    const NEXT_ICON_IDENTIFIER = 'ic_next'; // for next chapter icon
    const AUTHOR_SAY_SELECTOR = 'div.h-reader div.postscript > div.value'; // for author say

    // Internationalization
    const isZhCN = navigator.language.toLowerCase() === 'zh-cn' || 
                   document.documentElement.lang.toLowerCase() === 'zh-cn';
    
    const i18n = {
        copyText: isZhCN ? '复制文本' : 'Copy Content',
        copiedText: isZhCN ? '已复制!' : 'Copied!',
        nextChapter: isZhCN ? '下一章' : 'Next Chapter',
        noNextChapter: isZhCN ? '没有下一章' : 'No Next Chapter',
        includeAuthorSay: isZhCN ? '包含作家说' : 'Include Author Say',
        excludeAuthorSay: isZhCN ? '排除作家说' : 'Exclude Author Say'
    };

    // State variable for author say inclusion
    let includeAuthorSay = true;

    // Create GUI elements
    const gui = document.createElement('div');
    gui.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        background: white;
        padding: 15px;
        border: 1px solid #ccc;
        border-radius: 5px;
        box-shadow: 0 0 10px rgba(0,0,0,0.1);
        z-index: 9999;
        resize: both;
        overflow: visible;
        min-width: 350px;
        min-height: 250px;
        max-width: 100vw;
        max-height: 80vh;
        resize-origin: top-left;
        display: flex;
        flex-direction: column;
    `;

    // Add CSS for custom resize handle at top-left
    const style = document.createElement('style');
    style.textContent = `
        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        .resize-handle {
            position: absolute;
            width: 14px;
            height: 14px;
            top: 0;
            left: 0;
            cursor: nwse-resize;
            z-index: 10000;
            background-color: #888;
            border-top-left-radius: 5px;
            border-right: 1px solid #ccc;
            border-bottom: 1px solid #ccc;
        }

        .spinner-overlay {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(240, 240, 240, 0.8);
            display: none;
            justify-content: center;
            align-items: center;
            z-index: 10001;
        }
    `;
    document.head.appendChild(style);

    // Create resize handle
    const resizeHandle = document.createElement('div');
    resizeHandle.className = 'resize-handle';
    
    const output = document.createElement('textarea');
    output.style.cssText = `
        width: 100%;
        flex: 1;
        margin-bottom: 8px;
        resize: none;
        overflow: auto;
        box-sizing: border-box;
        min-height: 180px;
    `;
    output.readOnly = true;

    // Create button container for horizontal layout
    const buttonContainer = document.createElement('div');
    buttonContainer.style.cssText = `
        display: flex;
        justify-content: center;
        gap: 10px;
        margin-bottom: 2px;
    `;

    // Create toggle author say button
    const toggleAuthorSayButton = document.createElement('button');
    toggleAuthorSayButton.textContent = includeAuthorSay ? i18n.excludeAuthorSay : i18n.includeAuthorSay;
    toggleAuthorSayButton.style.cssText = `
        padding: 4px 12px;
        cursor: pointer;
        background-color: #fbbc05; /* Yellow */
        color: white;
        border: none;
        border-radius: 15px;
        font-weight: bold;
        font-size: 0.9em;
    `;

    const copyButton = document.createElement('button');
    copyButton.textContent = i18n.copyText;
    copyButton.style.cssText = `
        padding: 4px 12px;
        cursor: pointer;
        background-color: #4285f4;
        color: white;
        border: none;
        border-radius: 15px;
        font-weight: bold;
        font-size: 0.9em;
    `;

    // Create next chapter button
    const nextChapterButton = document.createElement('button');
    nextChapterButton.textContent = i18n.nextChapter;
    nextChapterButton.style.cssText = `
        padding: 4px 12px;
        cursor: pointer;
        background-color: #34a853;
        color: white;
        border: none;
        border-radius: 15px;
        font-weight: bold;
        font-size: 0.9em;
    `;

    // Add buttons to container
    buttonContainer.appendChild(toggleAuthorSayButton);
    buttonContainer.appendChild(copyButton);
    buttonContainer.appendChild(nextChapterButton);

    // Create spinner overlay for better positioning
    const spinnerOverlay = document.createElement('div');
    spinnerOverlay.className = 'spinner-overlay';
    
    // Create spinner
    const spinner = document.createElement('div');
    spinner.style.cssText = `
        width: 30px;
        height: 30px;
        border: 4px solid rgba(0,0,0,0.1);
        border-radius: 50%;
        border-top-color: #333;
        animation: spin 1s ease-in-out infinite;
    `;
    
    spinnerOverlay.appendChild(spinner);

    // Add elements to GUI
    gui.appendChild(resizeHandle);
    gui.appendChild(output);
    gui.appendChild(buttonContainer);
    gui.appendChild(spinnerOverlay);
    document.body.appendChild(gui);

    // Custom resize functionality
    let isResizing = false;
    let originalWidth, originalHeight, originalX, originalY;

    resizeHandle.addEventListener('mousedown', (e) => {
        e.preventDefault();
        isResizing = true;
        originalWidth = parseFloat(getComputedStyle(gui).width);
        originalHeight = parseFloat(getComputedStyle(gui).height);
        originalX = e.clientX;
        originalY = e.clientY;
        
        document.addEventListener('mousemove', resize);
        document.addEventListener('mouseup', stopResize);
    });

    function resize(e) {
        if (!isResizing) return;
        
        const width = originalWidth - (e.clientX - originalX);
        const height = originalHeight - (e.clientY - originalY);
        
        if (width > 300 && width < window.innerWidth * 0.8) {
            gui.style.width = width + 'px';
            // Keep right position fixed and adjust left position
            gui.style.right = getComputedStyle(gui).right;
        }
        
        if (height > 250 && height < window.innerHeight * 0.8) {
            gui.style.height = height + 'px';
            // Keep bottom position fixed and adjust top position
            gui.style.bottom = getComputedStyle(gui).bottom;
        }
    }

    function stopResize() {
        isResizing = false;
        document.removeEventListener('mousemove', resize);
        document.removeEventListener('mouseup', stopResize);
    }
    
    // Helper function to find the next chapter link
    function findNextChapterLink() {
        // Find all navigation links
        const navLinks = document.querySelectorAll(NEXT_CHAPTER_BASE_SELECTOR);
        
        console.log(`Found ${navLinks.length} navigation link candidates`);
        
        // Look for the link with the next chapter icon
        for (const link of navLinks) {
            const iconImg = link.querySelector('img.iconfont');
            
            if (iconImg) {
                console.log(`Found icon with src: ${iconImg.src}`);
                
                if (iconImg.src && iconImg.src.includes(NEXT_ICON_IDENTIFIER)) {
                    console.log(`Found next chapter link: ${link.href}`);
                    return link;
                }
            }
        }
        
        console.log('No next chapter link found');
        return null; // No next chapter link found
    }

    // Legacy XPath extraction function (kept for fallback compatibility)
    function getElementsByXpath(xpath) {
        const results = [];
        const query = document.evaluate(
            xpath, 
            document, 
            null, 
            XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 
            null
        );
        
        for (let i = 0; i < query.snapshotLength; i++) {
            const node = query.snapshotItem(i);
            if (node) {
                // Get full text content including children, preserving whitespace
                const textContent = node.textContent; // Keep original whitespace
                // Only push if the content is not just whitespace
                if (textContent && textContent.trim()) {
                    results.push(textContent);
                }
            }
        }
        return results;
    }    
    // Initial extraction
    function updateTitleOutput() {
        // Use querySelector with the new selector for title
        const titleElement = document.querySelector(TITLE_SELECTOR);
        if (titleElement) {
            // Extract direct text content, similar to XPath approach
            let directTextContent = '';
            for (let i = 0; i < titleElement.childNodes.length; i++) {
                const childNode = titleElement.childNodes[i];
                if (childNode.nodeType === Node.TEXT_NODE) {
                    directTextContent += childNode.textContent;
                }
            }
            return directTextContent.trim();
        }
        return '';
    }

    function updateContentOutput(includeAuthorSayFlag) {
        // Use querySelector to get the content container
        const contentContainer = document.querySelector(CONTENT_SELECTOR);
        let elements = [];
        
        if (contentContainer) {
            // Get all p elements within the content container
            const paragraphs = contentContainer.querySelectorAll('p');
            
            // Process each paragraph, excluding those with the "watermark" class
            paragraphs.forEach(p => {
                // Skip paragraphs with the "watermark" class
                if (!p.classList.contains('watermark')) {
                    const textContent = p.textContent;
                    // Only add paragraphs that have non-whitespace content
                    if (textContent && textContent.trim()) {
                        elements.push(textContent);
                    }
                }
            });
        }

        if (elements.length === 0) {
            console.error('no elements found for content, maybe using canvas');
        }
        
        // Join elements, do not trim here to preserve first line indentation
        let content = elements.join('\n');

        // Append author say if requested
        if (includeAuthorSayFlag) {
            // Use querySelector for author say with the new selector
            const authorSayElement = document.querySelector(AUTHOR_SAY_SELECTOR);
            let authorSayContent = '';
            
            if (authorSayElement) {
                authorSayContent = authorSayElement.textContent.trim();
            }
            
            // Add author say content if it exists
            if (authorSayContent) {
                // Add separation if both content and author say exist
                if (content.trim()) {
                    content += '\n\n---\n\n' + authorSayContent; // Add separator
                } else {
                    content = authorSayContent;
                }
            }
        }

        return content; // Return potentially leading-whitespace content
    }

    // Async update function
    async function updateOutput() {
        // Show spinner overlay
        spinnerOverlay.style.display = 'flex';
        
        // Use setTimeout to make it async and not block the UI
        setTimeout(() => {
            try {
                const title = updateTitleOutput();
                const content = updateContentOutput(includeAuthorSay); // Pass the state
                output.value = title ? title + '\n\n' + content : content;
            } catch (error) {
                console.error('Error updating output:', error);
            } finally {
                // Hide spinner when done
                spinnerOverlay.style.display = 'none';
            }
        }, 0);
    }

    // Run initial extraction
    updateOutput();

    // Add event listener for toggle author say button
    toggleAuthorSayButton.addEventListener('click', () => {
        includeAuthorSay = !includeAuthorSay; // Toggle state
        toggleAuthorSayButton.textContent = includeAuthorSay ? i18n.excludeAuthorSay : i18n.includeAuthorSay;
        updateOutput(); // Update the content
    });

    // Add event listener for copy button
    copyButton.addEventListener('click', () => {
        output.select();
        document.execCommand('copy');
        copyButton.textContent = i18n.copiedText;
        setTimeout(() => {
            copyButton.textContent = i18n.copyText;
        }, 1000);
    });

    // Add event listener for next chapter button
    nextChapterButton.addEventListener('click', () => {
        // Find the next chapter link using our helper function
        const nextChapterLink = findNextChapterLink();
        
        if (nextChapterLink) {
            // Navigate to the next chapter
            window.location.href = nextChapterLink.href;
        } else {
            // Show a message if there's no next chapter
            nextChapterButton.textContent = i18n.noNextChapter;
            nextChapterButton.style.backgroundColor = '#ea4335';
            
            setTimeout(() => {
                nextChapterButton.textContent = i18n.nextChapter;
                nextChapterButton.style.backgroundColor = '#34a853';
            }, 2000);
        }
    });

    // Find the content container element to observe (using the content selector)
    const contentElement = document.querySelector(APP_WRAPPER_SELECTOR);
    
    // Setup MutationObserver to watch for changes
    if (contentElement) {
        const observer = new MutationObserver(() => {
            updateOutput();
        });
        
        observer.observe(contentElement, {
            childList: true,
            subtree: true,
            characterData: true
        });
        
        // Also observe the document body for any structural changes that might affect the content
        observer.observe(document.body, {
            childList: true,
            subtree: false // Only direct children of body
        });
    } else {
        console.error('Content element not found. Cannot setup observer.');
    }
})();