Changpei Chapter Downloader

Download chapter content from Changpei(gongzicp.com)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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              https://greasyfork.org/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.');
    }
})();