E站控制画廊已收藏显隐和黑名单

漫画资源e站,增加功能:1、控制已收藏画廊显隐 2、快速添加收藏功能 3、黑名单屏蔽重复、缺页、低质量画廊 4、详情页生成文件名

// ==UserScript==
// @name         E站控制画廊已收藏显隐和黑名单
// @namespace    http://tampermonkey.net/
// @version      1.5.3
// @license      GPL-3.0
// @description  漫画资源e站,增加功能:1、控制已收藏画廊显隐 2、快速添加收藏功能 3、黑名单屏蔽重复、缺页、低质量画廊 4、详情页生成文件名
// @author       ShineByPupil
// @match        *://exhentai.org/*
// @icon data:image/png;base64,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
// @grant        none
// ==/UserScript==

(async function () {
    'use strict';

    // 【文件名去除规则】
    const parenthesesRule = '\\([^(]*(' +
        ['Vol', 'COMIC', '成年コミック', 'C\\d+', 'よろず', 'FF\\d+', '\\d{4}年\\d{1,2}月', 'Chinese', '机翻', 'コミック', '汉化组', '中文'].join('|') +
        ')[^(]*\\)'; // 圆括号
    const squareBracketsRule = '\\[[^[]*(' +
        ['汉化', '漢化', '翻訳', 'Chinese', 'chinese', 'CHINESE', '無修正', 'DL版', '中国語', '中文', '渣翻', '机翻', '機翻', '重嵌'].join('|') +
        ')[^[]*\\]'; // 方括号
    let isFilter = localStorage.getItem('isFilter') === 'true';
    let alwaysFilter = localStorage.getItem('alwaysFilter') || '';
    let favoriteList = await getFavorites(); // 收藏设置

    const utils = {
        messageBox: null,
        /**
         * 在屏幕上显示指定时间长度的消息。
         *
         * @param {string} message - 要显示的消息。
         * @param {number} [duration=2500] - 消息应显示的毫秒数。默认为2500毫秒。
         * @return {void} 此函数不返回值。
         */
        showMessage: function (message, duration = 2500) {
            if (!this.messageBox) {
                // 创建一个 Shadow Root
                this.createShadowMessageBox();
            }

            this.messageBox.textContent = message;
            this.messageBox.style.display = 'block'; // 显示消息

            // 设置一定时间后自动隐藏消息
            setTimeout(() => {
                this.messageBox.style.display = 'none';
            }, duration);
        },
        /**
         * 从提供的模板字符串创建一个新的 DOM 节点。
         *
         * @param {string} template - 要创建节点的 HTML 模板字符串。
         * @return {Node} 新创建的 DOM 节点。
         */
        createNode: function (template) {
            const div = document.createElement('div');
            div.innerHTML = template.trim();
            return div.firstChild;
        },
        /**
         * 创建一个带有 Shadow DOM 的消息框。
         *
         * @return {void}
         */
        createShadowMessageBox: function () {
            const container = document.createElement('div');
            const shadowRoot = container.attachShadow({mode: 'open'});

            // 创建消息框的样式,使用明亮的配色
            const style = document.createElement('style');
            style.textContent = `
            #messageBox {
                position: fixed;
                top: 20px;
                left: 50%;
                transform: translateX(-50%);
                background-color: #ffffff; /* 明亮的背景色 */
                color: #000000; /* 深色文本 */
                padding: 10px 20px;
                border-radius: 5px;
                z-index: 1000;
                display: none; /* 初始隐藏 */
                transition: opacity 0.3s ease;
                opacity: 1;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); /* 添加阴影效果 */
            }
        `;

            // 创建消息框节点
            const messageBox = document.createElement('div');
            messageBox.id = 'messageBox';

            // 将样式和消息框添加到 Shadow DOM
            shadowRoot.appendChild(style);
            shadowRoot.appendChild(messageBox);

            // 将包含 Shadow DOM 的容器添加到文档中
            document.body.appendChild(container);

            // 保存对消息框的引用
            this.messageBox = messageBox;
        },
    };

    // 根据 URL 执行不同的代码
    if (['/', '/watched', '/popular'].includes(window.location.pathname)) {
        // 主页
        filterFavorites();
        setFavorites();
    } else if (window.location.pathname === '/favorites.php') {
        setFavorites();
    } else if (/^\/g\/\d+\/[a-z0-9]+\/$/.test(window.location.pathname)) {
        // 详情页
        formatFileName();
        setFavorites();
    } else if (/^\/tag\/.*$/.test(window.location.pathname)) {
        filterFavorites();
        setFavorites();
    }

    // 右下角按钮组:收藏显隐、总是过滤、过滤全部
    function filterFavorites() {
        const div = document.createElement('div');
        const refreshBtn = document.createElement('button');
        refreshBtn.innerText = '↻刷新';
        refreshBtn.addEventListener('click', function () {
            location.reload();
        });
        const toggleBtn = document.createElement('button');
        toggleBtn.innerText = isFilter ? '点击显示' : '点击隐藏';
        toggleBtn.addEventListener('click', function () {
            isFilter = !isFilter;
            localStorage.setItem('isFilter', isFilter);
            toggleBtn.innerText = isFilter ? '点击显示' : '点击隐藏';

            handleFilter();
        });
        const filterBtn = document.createElement('button');
        filterBtn.innerText = '总是过滤';
        filterBtn.addEventListener('click', function () {
            const userInput = prompt("请输入总是过滤的收藏名:", alwaysFilter);

            if (userInput !== null) {
                alwaysFilter = userInput;
                localStorage.setItem('alwaysFilter', alwaysFilter);
                handleFilter();
            }

        });
        const filterAllBtn = document.createElement('button');
        filterAllBtn.innerText = '过滤全部';
        filterAllBtn.addEventListener('click', async function () {
            if (!alwaysFilter) {
                return utils.showMessage('请先设置总是过滤');
            }

            const index = favoriteList.indexOf(alwaysFilter);
            if (index !== -1) {
                const list = Array.from(
                    document
                        .querySelector('.itg')
                        .querySelectorAll('div[id^="posted_"]')
                )
                    .filter(n => n.title === '')
                    .map(n => {
                        const matches = n.onclick.toString().match(/gid=(\d+)&t=([a-z0-9]+)/);
                        const [,gid,t] = matches;
                        return { gid, t };
                    })

                // 处理并发请求
                const set = new Set();
                const enqueue = async function (promise) {
                    if (set.size > 5) {
                        await Promise.race(set);
                    }

                    const p = promise().finally(() => set.delete(p));
                    set.add(p);
                    return p;
                }


                await Promise.all(
                    list.map(({ gid, t }) => {
                        return enqueue(() => updateFavorites(index, gid, t));
                    })
                )

                utils.showMessage('过滤全部成功');
            }
        });

        const divStyle = {
            position: 'fixed', // 绝对定位
            right: '10px', // 距离左边10像素
            bottom: '10px', // 距离顶部10像素
            zIndex: '1000', // 确保按钮在其他元素之上
            display: 'flex',
            flexDirection: 'column',
        }
        const btnStyle = {
            backgroundColor: '#007BFF', // 按钮背景颜色
            color: '#FFFFFF', // 按钮文字颜色
            border: 'none',
            borderRadius: '5px',
            cursor: 'pointer',
            padding: '4px 10px',
            marginBottom: '10px',
        };

        for (let key in divStyle) {
            div.style[key] = divStyle[key];
        }
        for (let key in btnStyle) {
            refreshBtn.style[key] = btnStyle[key];
            toggleBtn.style[key] = btnStyle[key];
            filterBtn.style[key] = btnStyle[key];
            filterAllBtn.style[key] = btnStyle[key];
        }

        // 添加按钮到页面
        div.appendChild(refreshBtn);
        div.appendChild(toggleBtn);
        div.appendChild(filterBtn);
        div.appendChild(filterAllBtn);
        document.body.appendChild(div);
        handleFilter();

        window.addEventListener('storage', function (event) {
            if (event.key === 'isFilter') {
                isFilter = event.newValue === 'true';
                toggleBtn.innerText = isFilter ? '点击显示' : '点击隐藏';
                handleFilter();
            }
        })
        const observer = new MutationObserver(mutationsList => {
            const domSet = new WeakSet();

            for (let mutation of mutationsList) {
                if (/^posted_\d+$/.test(mutation.target.id) && !domSet.has(mutation.target)) {
                    domSet.add(mutation.target);
                    handleFilter();
                }
            }
        })

        // 开始观察目标节点
        const targetNode = document.querySelector('.itg');
        if (targetNode) {
            observer.observe(targetNode, {
                attributes: true,
                subtree: true
            });
        }
    }

    // 开始过滤
    function handleFilter() {
        const list = document.querySelector('table.itg')
            ? document.querySelectorAll('table.itg tr')
            : document.querySelectorAll('.itg.gld .gl1t');

        [...list].forEach(n => {
            const find = n.querySelector('[id^="posted_"]')

            if (find && find.title !== '') {
                if (alwaysFilter === find.title) {
                    n.style.display = 'none';
                } else {
                    n.style.display = isFilter ? 'none' : '';
                }
            }
        });
    }

    // 生成文件名成
    async function formatFileName() {
        const rule = new RegExp(`${parenthesesRule}|${squareBracketsRule}`, 'g');
        let title = document.querySelector('#gj').innerText || document.querySelector('#gn').innerText;

        title = title
            .replace(/[[]()]/g, match => {
                if (match === '[') {
                    return '[';
                } else if (match === ']') {
                    return ']';
                } else if (match === '(') {
                    return '('
                } else if (match === ')') {
                    return ')'
                }
            })
            .replace(/[\/\\:*?"<>|]/g, ' ')
            .replace(rule, '')
            .replace(/\s+/g, ' ')
            .trim();

        const tagConfigMap = await fetch('https://exhentai.org/mytags')
            .then(r => r.text())
            .then(r => {
                const parser = new DOMParser();

                return parser.parseFromString(r, 'text/html');
            })
            .then(doc => {
                let map = new Map();
                // 没有关注和隐藏的标签(也希望显示在文件名)
                map.set('other:extraneous ads', { weight: -10 });
                map.set('other:incomplete', { weight: -11 });

                [...doc.querySelectorAll('#usertags_outer>div')].forEach(n => {
                    if (n.querySelector('.gt') && n.querySelector('input[id^=tagwatch]')?.checked) {
                        map.set(
                            n.querySelector('.gt').title,
                            { weight: parseInt(n.querySelector('[id^=tagweight]').value, 10) }
                        );
                    }
                });

                return map;
            });

        const tagDom = Array.from(document.querySelectorAll('#taglist a'));

        const formatId = id => id.slice(3).replace(/_/g, ' ');
        let tags = [...new Set(
            tagDom.filter(n => tagConfigMap.has(formatId(n.id)))
                .sort((n, m) => tagConfigMap.get(formatId(m.id)).weight - tagConfigMap.get(formatId(n.id)).weight)
                .map(n => `[${n.innerText}]`)
        )].join('');

        const input = document.createElement('input');
        input.style.width = '100%';
        input.style.textAlign = 'center';
        input.value = (title + ' ' + tags).trim();

        const button = document.createElement('button');
        button.onclick = function () {
            navigator.clipboard.writeText(input.value);
        }
        button.innerText = '复制';

        document.querySelector('#gd2').appendChild(input);
        document.querySelector('#gd2').appendChild(button);
    }

    // 快速收藏按钮组(鼠标悬停画廊封面)
    async function setFavorites() {
        const ulStyle = {
            margin: '0',
            padding: '0',
            display: 'none',
            flexDirection: 'column',
            position: 'absolute',
            zIndex: '1000',
        }
        const liStyle = {
            listStyleType: 'none',
            backgroundColor: '#007BFF',
            color: '#FFFFFF',
            cursor: 'pointer',
            padding: '2px 4px',
            margin: '2px 0',
            borderRadius: '5px',
            textAlign: 'center',
        }

        let gid = null;
        let t = null;

        const ulNode = utils.createNode(`<ul></ul>`);
        const favdelLi = utils.createNode(`<li>取消收藏</li>`);
        const refreshLi = utils.createNode(`<li>↻刷新</li>`);
        const favoriteLi = await createFavoriteLi();

        for (let key in ulStyle) {
            ulNode.style[key] = ulStyle[key];
        }
        for (let key in liStyle) {
            favdelLi.style[key] = liStyle[key];
            refreshLi.style[key] = liStyle[key];
        }

        ulNode.addEventListener('mouseover', function () {
            ulNode.style.display = 'flex';
        })
        ulNode.addEventListener('mouseout', function () {
            ulNode.style.display = 'none';
        });
        favdelLi.addEventListener('click', function () {
            if (gid && t) {
                updateFavorites('favdel', gid, t);
            }
        });
        refreshLi.addEventListener('click', async function () {
            ulNode.style.display = 'none';
            const favoriteLi = await createFavoriteLi(true);

            while (ulNode.children.length > 2) {
                ulNode.removeChild(ulNode.firstChild);
            }

            ulNode.insertBefore(favoriteLi, ulNode.firstChild);
            ulNode.style.display = 'flex';
        });

        ulNode.appendChild(favoriteLi);
        ulNode.appendChild(favdelLi);
        ulNode.appendChild(refreshLi);
        document.body.appendChild(ulNode);

        // 搜索主页
        const itgNode = document.querySelector('.itg');
        if (itgNode) {
            itgNode.addEventListener('mouseover', function (event) {
                const {target} = event;

                if (target.tagName === 'IMG' && target.alt !== 'T') {
                    const href = target.parentNode.href;
                    const groups = href.split('/');

                    gid = groups[groups.length - 3];
                    t = groups[groups.length - 2];

                    const rect = target.parentNode.parentNode.getBoundingClientRect();
                    ulNode.style.display = 'flex'
                    ulNode.style.left = `${rect.left + 10 + window.scrollX}px`;
                    ulNode.style.top = `${rect.top + 10 + window.scrollY}px`; // 在 li 下方显示
                }
            });
            itgNode.addEventListener('mouseout', function (e) {
                const {target} = e;

                if (target.tagName === 'IMG' && !ulNode.matches(':hover')) {
                    gid = null;
                    t = null;

                    ulNode.style.display = 'none';
                }
            });
        }

        // 详情页
        const cover = document.querySelector('#gd1 div');
        if (cover) {
            const groups = location.pathname.split('/');
            gid = groups[groups.length - 3];
            t = groups[groups.length - 2];

            cover.addEventListener('mouseover', function (event) {
                const rect = event.target.getBoundingClientRect();
                ulNode.style.display = 'flex'
                ulNode.style.left = `${rect.left + 10 + window.scrollX}px`;
                ulNode.style.top = `${rect.top + 10 + window.scrollY}px`; // 在 li 下方显示
            });
            cover.addEventListener('mouseout', function (event) {
                if (!ulNode.matches(':hover')) {
                    ulNode.style.display = 'none';
                }
            });
        }

        async function createFavoriteLi(disableCache = false) {
            const fragment = document.createDocumentFragment();
            if (disableCache) {
                favoriteList = await getFavorites(true);
            }

            favoriteList.forEach((n, index) => {
                if (!/^Favorites \d$/.test(n)) {
                    const liNode = utils.createNode(`<li>${n}</li>`);
                    liNode.addEventListener('click', async function () {
                        if (gid && t) {
                            await updateFavorites(index, gid, t);
                            handleFilter();
                            utils.showMessage('收藏成功');
                        }
                    })

                    for (let key in liStyle) {
                        liNode.style[key] = liStyle[key];
                    }

                    fragment.appendChild(liNode);
                }
            });

            return fragment;
        }
    }

    // API:获取收藏配置列表
    async function getFavorites(disableCache = false) {
        let favoriteList = localStorage.getItem('favoriteList');

        if (favoriteList && disableCache === false) {
            return JSON.parse(favoriteList);
        } else {
            const response = await fetch('https://exhentai.org/uconfig.php');
            const domStr = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(domStr, 'text/html');

            const list = Array.from(
                doc.querySelectorAll('#favsel input')
            ).map(n => n.value);

            if (list.length) {
                localStorage.setItem('favoriteList', JSON.stringify(list));
                return list;
            } else {
                throw new Error(doc.body.innerText)
            }
        }
    }

    // API:更新收藏
    async function updateFavorites(type, gid, t) {
        const formData = new FormData();
        formData.append('favcat', type);
        formData.append('favnote', '');
        formData.append('update', '1');

        const response = await fetch(
            `https://exhentai.org/gallerypopups.php?gid=${gid}&t=${t}&act=addfav`,
            {method: 'POST', body: formData});

        const domStr = await response.text();
        const parser = new DOMParser();
        const doc = parser.parseFromString(domStr, 'text/html');
        const script = Array.from(doc.querySelectorAll('script'))
            .find(n => n.textContent.includes('window.close()'));

        if (script) {
            let codeStr = script.textContent
            codeStr = codeStr.replace(/window.opener.document/g, 'window.document');
            codeStr = codeStr.replace(/window.close\(\);/g, '');


            const dynamicFunction = new Function(codeStr);
            dynamicFunction();
        }
    }
})();