Greasy Fork

章节讨论吐槽加强

章节讨论中置顶显示自己的吐槽,高亮回复过的章节格子

// ==UserScript==
// @name         章节讨论吐槽加强
// @namespace    https://bgm.tv/group/topic/408098
// @version      0.2.2
// @description  章节讨论中置顶显示自己的吐槽,高亮回复过的章节格子
// @author       oo
// @include      http*://bgm.tv/*
// @include      http*://chii.in/*
// @include      http*://bangumi.tv/*
// @license      MIT
// ==/UserScript==

(async function () {

    const user = document.querySelector('.avatar').href.split('/').pop();

    const style = document.createElement('style');
    style.textContent = `
        a.load-epinfo.epBtnWatched {
            opacity: .6;
        }
        a.load-epinfo.epBtnWatched.commented {
            opacity: 1;
            background: blueviolet;
        }
        a.load-epinfo.epBtnWatched.uncommented {
            opacity: 1;
        }
        a.load-epinfo.epBtnAir.commented {
            background: #cfa0fc;
        }
        html[data-theme="dark"] a.load-epinfo.epBtnWatched.commented {
            background: blueviolet;
        }
        html[data-theme="dark"] a.load-epinfo.epBtnAir.commented {
            background: #cfa0fc;
        }
    `;
    document.head.appendChild(style);

    async function getDOM(url) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error('HTTP request failed');
            const html = await response.text();
            const dom = new DOMParser().parseFromString(html, 'text/html');
            return dom;
        } catch (error) {
            console.error('章节讨论置顶自己的吐槽: Error fetching and parsing page:', error);
        }
    }

    const cacheHandler = {
        // 初始化时检查并清理过期项目
        init(target) {
            const data = JSON.parse(localStorage.getItem(target.storageKey) || '{}');
            const now = Date.now();
            for (const key in data) {
                if (data[key].expiry < now) {
                    delete data[key];
                }
            }
            localStorage.setItem(target.storageKey, JSON.stringify(data));
        },
        get(target, key) {
            const data = JSON.parse(localStorage.getItem(target.storageKey) || '{}');
            const now = Date.now();
            const oneMonth = 30 * 24 * 60 * 60 * 1000;

            if (data[key] && now < data[key].expiry) {
                // 调用时延后一个月过期时间
                data[key].expiry = now + oneMonth;
                localStorage.setItem(target.storageKey, JSON.stringify(data));
                return data[key].value;
            } else {
                delete data[key];
                localStorage.setItem(target.storageKey, JSON.stringify(data));
                return undefined;
            }
        },
        set(target, key, value) {
            const now = Date.now();
            const oneMonth = 30 * 24 * 60 * 60 * 1000;
            const expiry = now + oneMonth;

            const data = JSON.parse(localStorage.getItem(target.storageKey) || '{}');
            data[key] = { value, expiry };
            localStorage.setItem(target.storageKey, JSON.stringify(data));

            return true;
        }
    };

    const cacheTarget = { storageKey: 'incheijs_ep_cache' };
    cacheHandler.init(cacheTarget);
    const cache = new Proxy(cacheTarget, cacheHandler);

    // 章节讨论页
    if (location.pathname.startsWith('/ep')) {
        let replies = getRepliesFromDOM(document);
        const id = location.pathname.split('/')[2];
        if (replies[0]) {
            document.getElementById('reply_wrapper').before(...replies.map(elem => {
                const clone = elem.cloneNode(true);
                clone.id += '_clone';
                clone.classList.add('cloned_mine');
                clone.querySelectorAll('.likes_grid a').forEach(a => {
                    a.href = 'javascript:';
                    a.style.cursor = 'default';
                }); // 防止点击贴贴无效跳转首页
                clone.style.setProperty('display', 'block', 'important'); // 兼容开播前隐藏
                return clone;
            }));
            cache[id] = true;
        } else {
            cache[id] = false;
        }
        // 兼容开播前隐藏
        document.querySelector('#comments_seperater')?.addEventListener('click', () => {
            document.querySelectorAll('.cloned_mine').forEach(e => e.style.setProperty('display', 'block', 'important'));
        });
        // 添加回复
        document.querySelector('#ReplyForm').addEventListener('submit', async () => {
            const observer = new MutationObserver(() => {
                // 因 AJAX 添加的元素未设置 dataset,不可用 getRepliesFromDOM
                const myReplies = [...document.querySelectorAll('#comment_list .row_reply')].filter(comment => comment.querySelector('.avatar').href.split('/').pop() === user);
                if (myReplies.length) {
                    cache[id] = true;
                    observer.disconnect();
                }
            });
            observer.observe(document.querySelector('#comment_list'), { childList: true });
        });
        // 侧栏其他章节,无法直接判断是否看过,只取缓存不检查
        const epElems = document.querySelectorAll('.sideEpList li a');
        for (const elem of epElems) {
            const url = elem.href;
            const id = url.split('/')[4];
            if (cache[id] === true) elem.style.color = 'blueviolet';
        }
    }

    function getRepliesFromDOM(dom) {
        return [...dom.querySelectorAll('#comment_list .row_reply')].filter(comment => comment.dataset.itemUser === user);
    }

    // 动画条目页
    const subjectID = location.pathname.match(/(?<=subject\/)\d+/)?.[0];
    if (subjectID) {
        const type = document.querySelector('.focus').href.split('/')[3];
        if (['anime', 'real'].includes(type)) {
            renderChecks();
        }
    }

    // 首页
    if (location.pathname === '/') {
        renderChecks();
    }

    async function retryAsyncOperation(operation, maxRetries = 3, delay = 1000) {
        let error;
        for (let i = 0; i < maxRetries; i++) {
            try {
                return await operation();
            } catch (e) {
                error = e;
                if (i < maxRetries - 1) {
                    await new Promise(resolve => setTimeout(resolve, delay));
                }
            }
        }
        throw error;
    }

    async function limitConcurrency(tasks, concurrency = 2) {
        const results = [];
        let index = 0;

        async function runTask() {
            while (index < tasks.length) {
                const currentIndex = index++;
                const task = tasks[currentIndex];
                try {
                    const result = await task();
                    results[currentIndex] = result;
                } catch (error) {
                    results[currentIndex] = error;
                }
            }
        }

        const runners = Array.from({ length: concurrency }, runTask);
        await Promise.all(runners);
        return results;
    }

    function generateCacheKey(url) {
        return new URL(url).pathname.split('/').filter(part => part).pop();
    }

    async function renderChecks() {
        const epElems = document.querySelectorAll('.load-epinfo');
        const tasks = [];

        for (const elem of epElems) {
            const url = elem.href;
            const id = generateCacheKey(url);

            const task = async () => {
                let hasComments;
                if (cache[id]!== undefined) {
                    hasComments = cache[id];
                } else if (elem.classList.contains('epBtnWatched')) {
                    try {
                        const dom = await retryAsyncOperation(() => getDOM(url));
                        hasComments = getRepliesFromDOM(dom).length > 0;
                        cache[id] = hasComments;
                    } catch (error) {
                        console.error(`Failed to fetch DOM for ${url}:`, error);
                        return;
                    }
                } else {
                    return;
                }

                elem.classList.add(hasComments ? 'commented' : 'uncommented');
            };

            tasks.push(task);
        }

        await limitConcurrency(tasks, 5);
    }

})();