Greasy Fork

Greasy Fork is available in English.

拷貝漫畫

清理符號,檢查連結訪問狀態,開啟未訪問的連結,並在章節頁面自動處理

当前为 2025-10-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         拷貝漫畫
// @namespace    http://tampermonkey.net/
// @version      16.2
// @description  清理符號,檢查連結訪問狀態,開啟未訪問的連結,並在章節頁面自動處理
// @match        https://mangacopy.com/comic/*
// @grant        none
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    // ===== 時間參數設定 =====
    const SCRIPT_START_DELAY = 3000;  // 目錄頁面腳本開始延遲時間(毫秒)
    const CHAPTER_PAGE_START_DELAY = 1000;  // 章節頁面腳本開始延遲時間(毫秒)
    const PROCESSING_LOCK_TIMEOUT = 5000;  // 處理鎖定超時時間(毫秒)
    const QUEUE_CHECK_INTERVAL = 1000;  // 隊列檢查間隔(毫秒)
    const NAVIGATION_DELAY = 200;  // 頁面跳轉延遲時間(毫秒)
    const LIST_ADD_DELAY = 200;  // 新增清單後的延遲時間(毫秒)

    // ===== 常量設定 =====
    const visitedPagesKey = 'visitedPages';
    const listPrefix = 'unvisitedLinks_';
    const queueKey = 'processingQueue';
    const scriptEnabledKey = 'scriptEnabled';
    const scriptDisabledKey = 'copyMangaCleanerDisabled';
    const processingLockKey = 'processingLock';
    const isChapterPage = window.location.href.includes('/chapter/');

    // ===== 通用函數 =====
    function showStatus(message) {
        let statusBar = document.getElementById('status-bar');
        if (!statusBar) {
            statusBar = document.createElement('div');
            statusBar.id = 'status-bar';
            statusBar.style.position = 'fixed';
            statusBar.style.top = '10px';
            statusBar.style.left = '10px';
            statusBar.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
            statusBar.style.color = 'white';
            statusBar.style.padding = '10px';
            statusBar.style.borderRadius = '5px';
            statusBar.style.zIndex = '9999';
            document.body.appendChild(statusBar);
        }
        statusBar.textContent = message;
    }

    async function sha256(message) {
        try {
            const msgBuffer = new TextEncoder().encode(message);
            const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
            return hashBuffer;
        } catch (error) {
            console.error('哈希生成失敗:', error);
            return null;
        }
    }

    function arrayBufferToBase64(buffer) {
        let binary = '';
        const bytes = new Uint8Array(buffer);
        for (let i = 0; i < bytes.byteLength; i++) {
            binary += String.fromCharCode(bytes[i]);
        }
        return btoa(binary);
    }

    // ===== 非章節頁面邏輯 =====
    if (!isChapterPage) {
        let scriptEnabled = localStorage.getItem(scriptEnabledKey) !== 'false';

        function cleanLinks() {
            const links = document.querySelectorAll('a[href*="/chapter/"]:not([href*="#"])');
            const visitedPages = new Set(JSON.parse(localStorage.getItem(visitedPagesKey) || '[]'));
            links.forEach(async link => {
                const originalHref = link.href;
                link.href = link.href.replace(/-(?=[^/]*$)/g, '');
                const hashBuffer = await sha256(link.href);
                if (!hashBuffer) return;
                const hashBase64 = arrayBufferToBase64(hashBuffer);
                if (visitedPages.has(hashBase64)) {
                    link.style.color = 'red';
                } else {
                    link.style.color = 'green';
                }
            });
        }

        async function checkAndStoreUnvisitedLinks() {
            if (!scriptEnabled) return;
            const currentPageUrl = window.location.href;
            const listKey = listPrefix + currentPageUrl;
            let queue = JSON.parse(localStorage.getItem(queueKey) || '[]');
            const unvisitedLinks = JSON.parse(localStorage.getItem(listKey) || '[]');

            if (queue.includes(listKey) && unvisitedLinks.length === 0) {
                localStorage.removeItem(listKey);
                queue = queue.filter(item => item !== listKey);
                localStorage.setItem(queueKey, JSON.stringify(queue));
                showStatus('清單已清理');
                return;
            }

            if (!queue.includes(listKey)) {
                const visitedPages = new Set(JSON.parse(localStorage.getItem(visitedPagesKey) || '[]'));
                const links = Array.from(document.querySelectorAll('a[href*="/chapter/"]:not([href*="#"])'));
                const newUnvisitedLinks = [];
                const allUnvisited = [];

                for (const link of links) {
                    const hashBuffer = await sha256(link.href);
                    if (!hashBuffer) continue;
                    const hashBase64 = arrayBufferToBase64(hashBuffer);
                    if (!visitedPages.has(hashBase64)) {
                        allUnvisited.push(link.href);
                        visitedPages.add(hashBase64);
                    }
                }

                if (allUnvisited.length > 0) {
                    // 16.2: 選取倒數第二個未訪問連結
                    let targetLink;
                    if (allUnvisited.length >= 2) {
                        targetLink = allUnvisited[allUnvisited.length - 2];
                    } else {
                        // 如果只有一個未訪問連結,則選取第一個(即最後一個)
                        targetLink = allUnvisited[allUnvisited.length - 1];
                    }
                    newUnvisitedLinks.push(targetLink);
                    newUnvisitedLinks.push(currentPageUrl);
                    localStorage.setItem(listKey, JSON.stringify(newUnvisitedLinks));
                    localStorage.setItem(visitedPagesKey, JSON.stringify([...visitedPages]));
                    queue.push(listKey);
                    localStorage.setItem(queueKey, JSON.stringify(queue));
                    showStatus('已加入隊列,等待' + (LIST_ADD_DELAY / 1000) + '秒...');
                    await new Promise(resolve => setTimeout(resolve, LIST_ADD_DELAY));
                } else {
                    return;
                }
            }

            const now = Date.now();
            const lock = JSON.parse(localStorage.getItem(processingLockKey) || 'null');
            if (lock && now - lock.timestamp < PROCESSING_LOCK_TIMEOUT) {
                showStatus('等待其他分頁處理完成...');
                setTimeout(checkAndStoreUnvisitedLinks, QUEUE_CHECK_INTERVAL);
                return;
            }

            if (queue[0] === listKey) {
                localStorage.setItem(processingLockKey, JSON.stringify({
                    timestamp: now,
                    listKey: listKey
                }));
                const storedLinks = JSON.parse(localStorage.getItem(listKey) || '[]');

                if (storedLinks.length === 0) {
                    localStorage.removeItem(listKey);
                    queue = queue.filter(item => item !== listKey);
                    localStorage.setItem(queueKey, JSON.stringify(queue));
                    localStorage.removeItem(processingLockKey);
                    showStatus('清單已清理');
                    return;
                }

                if (storedLinks[0] === currentPageUrl) {
                    localStorage.removeItem(listKey);
                    queue = queue.filter(item => item !== listKey);
                    localStorage.setItem(queueKey, JSON.stringify(queue));
                    localStorage.removeItem(processingLockKey);
                    showStatus('沒有更新');
                    return;
                }

                if (storedLinks.length > 0 && storedLinks[0]) {
                    // 16.2: 跳轉前清除 newUnvisitedLinks
                    // 這邊 storedLinks[0] 就是要跳轉的連結
                    const linkToNavigate = storedLinks[0];
                    // 清除 localStorage 中的 listKey (相當於清除 newUnvisitedLinks)
                    localStorage.removeItem(listKey);
                    // 從 queue 中移除 listKey
                    queue = queue.filter(item => item !== listKey);
                    localStorage.setItem(queueKey, JSON.stringify(queue));
                    // 清除鎖定
                    localStorage.removeItem(processingLockKey);

                    showStatus('正在處理連結...');
                    setTimeout(() => {
                        window.location.href = linkToNavigate;
                    }, NAVIGATION_DELAY);
                } else {
                    showStatus('錯誤:無有效連結');
                    localStorage.removeItem(processingLockKey);
                }
            } else {
                showStatus('正在排隊中...');
                setTimeout(checkAndStoreUnvisitedLinks, QUEUE_CHECK_INTERVAL);
            }
        }

        function addClearHistoryButton() {
            const clearButton = document.createElement('button');
            clearButton.textContent = '清除歷史';
            clearButton.style.position = 'fixed';
            clearButton.style.top = '60px';
            clearButton.style.left = '10px';
            clearButton.style.zIndex = '9999';
            clearButton.style.padding = '5px 10px';
            clearButton.style.backgroundColor = '#ff4444';
            clearButton.style.color = 'white';
            clearButton.style.border = 'none';
            clearButton.style.borderRadius = '5px';
            clearButton.style.cursor = 'pointer';
            clearButton.addEventListener('click', async () => {
                const links = Array.from(document.querySelectorAll('a[href*="/chapter/"]:not([href*="#"])'));
                const visitedPages = new Set(JSON.parse(localStorage.getItem(visitedPagesKey) || '[]'));
                await Promise.all(links.map(async link => {
                    const hashBuffer = await sha256(link.href);
                    if (!hashBuffer) return;
                    const hashBase64 = arrayBufferToBase64(hashBuffer);
                    if (visitedPages.has(hashBase64)) {
                        visitedPages.delete(hashBase64);
                    }
                }));
                localStorage.setItem(visitedPagesKey, JSON.stringify([...visitedPages]));
                showStatus('已清除當前頁面的連結記錄!');
            });
            document.body.appendChild(clearButton);
        }

        function addClearQueueButton() {
            const clearButton = document.createElement('button');
            clearButton.textContent = '清除隊列';
            clearButton.style.position = 'fixed';
            clearButton.style.top = '60px';
            clearButton.style.left = '100px';
            clearButton.style.zIndex = '9999';
            clearButton.style.padding = '5px 10px';
            clearButton.style.backgroundColor = '#ffaa44';
            clearButton.style.color = 'white';
            clearButton.style.border = 'none';
            clearButton.style.borderRadius = '5px';
            clearButton.style.cursor = 'pointer';
            clearButton.addEventListener('click', () => {
                const listKey = listPrefix + window.location.href.replace(/\/chapter\/.*/, '');
                const queue = JSON.parse(localStorage.getItem(queueKey) || '[]');
                localStorage.removeItem(listKey);
                const updatedQueue = queue.filter(item => item !== listKey);
                localStorage.setItem(queueKey, JSON.stringify(updatedQueue));
                showStatus('已清除當前隊列!');
            });
            document.body.appendChild(clearButton);
        }

        function addToggleButton() {
            const toggleButton = document.createElement('button');
            toggleButton.textContent = scriptEnabled ? '停用' : '啟用';
            toggleButton.style.position = 'fixed';
            toggleButton.style.top = '60px';
            toggleButton.style.left = '190px';
            toggleButton.style.zIndex = '9999';
            toggleButton.style.padding = '5px 10px';
            toggleButton.style.backgroundColor = scriptEnabled ? '#ff4444' : '#44aa44';
            toggleButton.style.color = 'white';
            toggleButton.style.border = 'none';
            toggleButton.style.borderRadius = '5px';
            toggleButton.style.cursor = 'pointer';
            toggleButton.addEventListener('click', () => {
                scriptEnabled = !scriptEnabled;
                localStorage.setItem(scriptEnabledKey, scriptEnabled);
                toggleButton.textContent = scriptEnabled ? '停用' : '啟用';
                toggleButton.style.backgroundColor = scriptEnabled ? '#ff4444' : '#44aa44';
                showStatus(scriptEnabled ? '腳本已啟用' : '腳本已停用');
                if (scriptEnabled) {
                    checkAndStoreUnvisitedLinks();
                }
            });
            document.body.appendChild(toggleButton);
        }

        function addDisableLinksButton() {
            const disableButton = document.createElement('button');
            disableButton.textContent = '停用連結';
            disableButton.style.position = 'fixed';
            disableButton.style.top = '60px';
            disableButton.style.left = '280px';
            disableButton.style.zIndex = '9999';
            disableButton.style.padding = '5px 10px';
            disableButton.style.backgroundColor = '#4444ff';
            disableButton.style.color = 'white';
            disableButton.style.border = 'none';
            disableButton.style.borderRadius = '5px';
            disableButton.style.cursor = 'pointer';
            disableButton.addEventListener('click', () => {
                const newState = localStorage.getItem(scriptDisabledKey) !== 'true';
                localStorage.setItem(scriptDisabledKey, newState.toString());
                showStatus(newState ? '章節頁面腳本已停用' : '章節頁面腳本已啟用');
            });
            document.body.appendChild(disableButton);
        }

        function runScript() {
            cleanLinks();
            addClearHistoryButton();
            addClearQueueButton();
            addToggleButton();
            addDisableLinksButton();
            if (scriptEnabled) {
                checkAndStoreUnvisitedLinks();
            } else {
                showStatus('腳本當前已停用');
            }
        }

        setTimeout(runScript, SCRIPT_START_DELAY);
    }

    // ===== 章節頁面邏輯 =====
    if (isChapterPage) {
        function getUnvisitedLinks() {
            const listKey = listPrefix + window.location.href.replace(/\/chapter\/.*/, '');
            return JSON.parse(localStorage.getItem(listKey) || '[]');
        }

        function openNextLink(unvisitedLinks) {
            if (unvisitedLinks.length > 0) {
                const nextLink = unvisitedLinks.shift();
                const listKey = listPrefix + window.location.href.replace(/\/chapter\/.*/, '');
                localStorage.setItem(listKey, JSON.stringify(unvisitedLinks));

                if (unvisitedLinks.length === 0) {
                    const parentUrl = window.location.href.replace(/\/chapter\/.*/, '');
                    showStatus('所有連結已處理完畢,返回目錄頁。');
                    setTimeout(() => {
                        window.location.href = parentUrl;
                    }, NAVIGATION_DELAY);
                } else {
                    showStatus('正在開啟下一個連結...');
                    setTimeout(() => {
                        window.location.href = nextLink;
                    }, NAVIGATION_DELAY);
                }
            }
        }

        function runScript() {
            const isDisabled = localStorage.getItem(scriptDisabledKey) === 'true';
            if (!isDisabled) {
                const unvisitedLinks = getUnvisitedLinks();
                if (unvisitedLinks.length > 0) {
                    openNextLink(unvisitedLinks);
                } else {
                    const parentUrl = window.location.href.replace(/\/chapter\/.*/, '');
                    showStatus('沒有未訪問的連結,返回目錄頁。');
                    setTimeout(() => {
                        window.location.href = parentUrl;
                    }, NAVIGATION_DELAY);
                }
            } else {
                showStatus('腳本當前已停用');
            }
        }

        setTimeout(runScript, CHAPTER_PAGE_START_DELAY);
    }
})();