色花堂 98堂 强化脚本

加强论坛功能

Mint 2024.08.16.. Lásd a legutóbbi verzió

// ==UserScript==
// @name         色花堂 98堂 强化脚本
// @namespace    http://tampermonkey.net/
// @version      0.0.8(2024-08-16)
// @description  加强论坛功能
// @license      MIT
// @author       98_ethan
// @match        *://*.sehuatang.net/*
// @match        *://*.sehuatang.org/*
// @match        *://*.sehuatang.*/*
// @match        *://*.jq2t4.com/*
// @match        *://*.0krgb.com/*
// @match        *://*.xxjsnc.co/*
// @match        *://*.o4vag.com/*
// @match        *://*.weterytrtrr.*/*
// @match        *://*.qweqwtret.*/*
// @match        *://*.retreytryuyt.*/*
// @match        *://*.qwerwrrt.*/*
// @match        *://*.ds5hk.app/*
// @match        *://*.30fjp.com/*
// @match        *://*.18stm.cn/*
// @match        *://*.xo6c5.com/*
// @match        *://*.mzjvl.com/*
// @match        *://*.9xr2.app/*
// @match        *://*.kzs1w.com/*
// @match        *://*.nwurc.com/*
// @match        *://*.zbkz6.app/*
// @match        *://*.ql75t.cn/*
// @match        *://*.0uzb0.app/*
// @match        *://*.d2wpb.com/*
// @match        *://*.5aylp.com/*
// @match        *://*.8otvk.app/*
// @match        *://*.05kx.cc/*
// @match        *://*.1yxg2.com/*
// @match        *://*.6r5gy.co/*
// @match        *://*.mmpbg.co/*
// @match        *://*.kofqo.com/*
// @match        *://*.kofqo.net/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @icon         https://sehuatang.net/favicon.ico
// ==/UserScript==

function initGM() {
    let gmExists = false;
    try {
        if (typeof GM.getValue == 'function') {
            gmExists = true;
        }
    } catch (ignore) { }

    if (gmExists) {
        return {
            getValue: GM.getValue,
            setValue: GM.setValue,
            async deleteValue(key) {
                return await GM.deleteValue(window.GM, key);
            },
            listValues: GM.listValues,
        };
    } else {
        return {
            getValue: GM_getValue,
            setValue: GM_setValue,
            async deleteValue(key) {
                return await GM_deleteValue(key);
            },
            listValues: GM_listValues,
        };
    }
}

const createLoadingIndicator = (message) => {
    const indicator = document.createElement('div');
    indicator.className = 'loading-indicator';
    indicator.textContent = message;
    return indicator;
};

(async function () {
    'use strict';
    const GM = initGM();
    /**
     * quick jump to important contents
     */
    const elementsToCheck = [
        { selector: '.locked strong', text: hasPurchased() ? '已购买' : '前往购买' },
        { selector: '.blockcode', text: '资源链接' },
        { selector: '#ak_rate', text: '评分', isClick: true }
    ];

    const createButton = ({ text, onClick, title, ariaLabel }) => {
        const button = document.createElement('button');
        button.innerText = text;
        button.className = 'quick-button';
        button.title = title;
        button.setAttribute('aria-label', ariaLabel);
        button.addEventListener('click', onClick);
        return button;
    };

    const style = document.createElement('style');
    style.textContent = `
    .quick-button-container {
        position: fixed;
        right: 10px;
        top: 50%;
        transform: translateY(-50%);
        z-index: 9999;
        display: flex;
        flex-direction: column;
        font-size: 1.1em;
        margin-top: 0;
        padding: 0.2em 0.5em;
        scrollbar-gutter: stable;
        scrollbar-width: thin;
        background: #FEF2E8;
    }

    .quick-button {
        margin: 5px;
        border: none;
        cursor: pointer;
        color: #787878;
        font-weight: bold;
        font-family: monospace;
        margin-right: 0.3em;
        min-width: 2.5em;
        display: inline-block;
        background: none;
    }
    .quick-button:hover { text-decoration: underline; }
    .favorite-button, .block-button {
        border: 1px solid;
        cursor: pointer;
    }
    .favorite-button {
        background: #FEAE10;
    }
`;
    document.head.appendChild(style);

    function hasPurchased() {
        return document.querySelector('.y a[href*="action=viewpayments"]') !== null;
    }

    const scrollToElement = (element) => {
        const observer = new MutationObserver((mutations, obs) => {
            setTimeout(() => {
                element.scrollIntoView({ behavior: 'smooth', block: 'center' });
                obs.disconnect();
            }, 800);
        });

        observer.observe(document.body, { childList: true, subtree: true });

        element.scrollIntoView({ behavior: 'smooth', block: 'center' });
    };

    const buttonContainer = document.createElement('div');
    buttonContainer.className = 'quick-button-container';

    const updateButtonStates = () => {
        buttonContainer.innerHTML = ''; // 清空现有按钮,防止重复添加

        elementsToCheck.forEach(({ selector, text, isClick }) => {
            const element = document.querySelector(selector);
            if (element) {
                const button = createButton({
                    text,
                    title: element.textContent.trim(),
                    ariaLabel: element.textContent.trim(),
                    onClick: () => {
                        if (isClick) {
                            element.click();
                            observeRateForm(); // 观察评分表单
                        } else {
                            scrollToElement(element);
                        }
                    }
                });
                buttonContainer.appendChild(button);
            }
        });

        // 更新附件按钮
        const updateAttachmentButtons = () => {
            // 更新常规附件按钮
            const attachmentElements = document.querySelectorAll('span[id^="attach_"]');
            attachmentElements.forEach(element => {
                const button = createButton({
                    text: '下载附件',
                    title: element.textContent.trim(),
                    ariaLabel: element.textContent.trim(),
                    onClick: () => scrollToElement(element)
                });
                buttonContainer.appendChild(button);
            });

            // 更新免费附件按钮
            const freeAttachmentElements = document.querySelectorAll('dl.tattl');
            freeAttachmentElements.forEach(element => {
                const tipElement = element.querySelector('.tip');
                if (tipElement && tipElement.textContent.includes('点击文件名下载附件')) {
                    const button = createButton({
                        text: '下载附件',
                        title: element.textContent.trim(),
                        ariaLabel: element.textContent.trim(),
                        onClick: () => scrollToElement(element)
                    });
                    buttonContainer.appendChild(button);
                }
            });
        };

        updateAttachmentButtons();
    };

    const observeRateForm = () => {
        const rateObserver = new MutationObserver((mutations, obs) => {
            const rateForm = document.querySelector('#rateform');
            if (rateForm) {
                const scoreElement = document.querySelector('#scoreoption8 li:first-child');
                if (scoreElement) {
                    const scoreValue = scoreElement.textContent.trim();
                    document.querySelector('#score8').value = scoreValue;
                }

                // 监听表单关闭事件
                const closeObserver = new MutationObserver((mutations, closeObs) => {
                    if (!document.querySelector('#rateform')) {
                        closeObs.disconnect();

                        // 监听 #postlist 内容更新
                        const postlistObserver = new MutationObserver((mutations, postObs) => {
                            // 确保更新已完成
                            setTimeout(() => {
                                updateButtonStates(); // 仅更新按钮状态
                                postObs.disconnect(); // 停止观察
                            }, 1500);
                        });

                        const postlist = document.querySelector('#postlist');
                        if (postlist) {
                            postlistObserver.observe(postlist, { childList: true, subtree: true });
                        }
                    }
                });

                closeObserver.observe(document.body, { childList: true, subtree: true });

                obs.disconnect(); // 停止观察表单
            }
        });

        rateObserver.observe(document.body, { childList: true, subtree: true });
    };

    // 初次更新按钮状态
    updateButtonStates();

    if (buttonContainer.children.length > 0) {
        document.body.appendChild(buttonContainer);
    }

    /**
   * Highling favorite users and today's new posts
   */
    const currentUrl = window.location.href;
    const isUserProfilePage = /mod=space&uid=\d+|space-uid-\d+/.test(currentUrl);
    const isPostListPage = /fid=\d+/.test(currentUrl);

    const createToggleKVStore = async (storeName) => {
        let data = await GM.getValue(storeName, {});
        const updateStore = async () => {
            await GM.setValue(storeName, data);
        };
        return { data, updateStore }
    }

    const { data: favoriteUsers, updateStore: updateFavoriteUsers } = await createToggleKVStore('favoriteUsers')
    const { data: blockedUsers, updateStore: updateBlockedUsers } = await createToggleKVStore('blockedUsers')

    if (isUserProfilePage) {
        const userProfileSelector = '#uhd .mt';
        const userProfileElement = document.querySelector(userProfileSelector);
        if (userProfileElement) {
            const username = userProfileElement.textContent.trim();
            let isFavorited = !!favoriteUsers[username];
            let isBlocked = !!blockedUsers[username];

            const favoriteButton = document.createElement('button');
            favoriteButton.innerText = isFavorited ? '取消收藏' : '收藏用户';
            favoriteButton.className = 'quick-button favorite-button';
            favoriteButton.style.marginLeft = '10px';

            favoriteButton.addEventListener('click', () => {
                if (isFavorited) {
                    if (confirm(`确定取消收藏用户 ${username} 吗?`)) {
                        delete favoriteUsers[username];
                        favoriteButton.innerText = '收藏用户';
                        isFavorited = false;
                    }
                } else {
                    if (confirm(`确定收藏用户 ${username} 吗?`)) {
                        favoriteUsers[username] = true;
                        favoriteButton.innerText = '取消收藏';
                        isFavorited = true;

                        delete blockedUsers[username];
                        blokedButton.innerText = '屏蔽用户';
                        isBlocked = false;

                    }
                }
                updateFavoriteUsers();
                updateBlockedUsers();
            });

            const blokedButton = document.createElement('button');
            blokedButton.innerText = isBlocked ? '取消屏蔽' : '屏蔽用户';
            blokedButton.className = 'quick-button block-button';
            blokedButton.style.marginLeft = '10px';

            blokedButton.addEventListener('click', () => {
                if (isBlocked) {
                    if (confirm(`确定取消屏蔽用户 ${username} 吗?`)) {
                        delete blockedUsers[username];
                        blokedButton.innerText = '屏蔽用户';
                        isBlocked = false;

                    }
                } else {
                    if (confirm(`确定屏蔽用户 ${username} 吗?`)) {
                        blockedUsers[username] = true;
                        blokedButton.innerText = '取消屏蔽';
                        isBlocked = true;

                        delete favoriteUsers[username];
                        favoriteButton.innerText = '收藏用户';
                        isFavorited = false;
                    }
                }
                updateBlockedUsers();
                updateFavoriteUsers();
            });

            userProfileElement.appendChild(favoriteButton);
            userProfileElement.appendChild(blokedButton);
        }
    }

    const highOrHidePostLists = () => {
        const postsList = document.querySelectorAll('tbody[id^="normalthread_"]')
        postsList.forEach(postItem => {
            const postElement = postItem.querySelector('tr td.by:nth-child(3)');
            const citeElement = postElement.querySelector('cite a');
            const topicTimeSpan = postElement.querySelector('span.xi1 span');

            // Highling favorite users
            if (citeElement && favoriteUsers[citeElement.textContent.trim()]) {
                citeElement.style.fontWeight = 'bold';
                citeElement.style.color = 'dodgerblue'; // Change color to dodgerblue
            }

            // Hide blocked users' posts
            if (citeElement && blockedUsers[citeElement.textContent.trim()]) {
                postItem.style.display = 'none';
            }

            // Highlight today's new posts
            const today = new Date().toISOString().split('T')[0];
            if (topicTimeSpan && topicTimeSpan.title === today) {
                topicTimeSpan.style.fontWeight = 'bold';
            }
        })
    };

    const isHomepage = window.location.pathname === '/';

    if (isHomepage) {
        const posts = document.querySelectorAll('.dxb_bc li')
        posts.forEach(post => {
            const username = post.querySelector('em a');
            // Highling favorite users
            if (username && favoriteUsers[username.textContent.trim()]) {
                username.style.fontWeight = 'bold';
                username.style.color = 'dodgerblue'; // Change color to dodgerblue
            }

            // Hide blocked users' posts
            if (username && blockedUsers[username.textContent.trim()]) {
                post.style.display = 'none';
            }
        })
    }

    if (isPostListPage) {
        // Initial call to highlight already loaded content
        highOrHidePostLists();

        // Use a MutationObserver to handle dynamically loaded content
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                highOrHidePostLists();
            });
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    const isSearchPage = /\/search\.php\?.*searchmd5=.*/.test(currentUrl);
    if (isSearchPage) {
        const { data: hiddenSections, updateStore: updateHiddenSections } = await createToggleKVStore('hiddenSections');

        const threadList = document.querySelectorAll('#threadlist .pbw');
        if (!threadList.length) return;

        const sectionMap = new Map();

        const processThread = (thread) => {
            const sectionLink = thread.querySelector('a[href*="fid="]');
            if (sectionLink) {
                const fid = new URL(sectionLink.href).searchParams.get('fid');
                const sectionName = sectionLink.textContent.trim();

                if (!sectionMap.has(fid)) {
                    sectionMap.set(fid, { name: sectionName, elements: [] });
                }
                sectionMap.get(fid).elements.push(thread);
            }
        };

        const applyFilterToNewThreads = (newThreads) => {
            sectionMap.forEach((section, fid) => {
                if (hiddenSections[fid]) {
                    newThreads.forEach(thread => {
                        const link = thread.querySelector(`a[href*="fid=${fid}"]`);
                        if (link) thread.style.display = 'none';
                    });
                }
            });
        };

        threadList.forEach(processThread);

        applyFilterToNewThreads(threadList);

        const updateFilterButtons = () => {
            sectionMap.forEach((section, fid) => {
                const existingButton = document.querySelector(`.filter-button[data-fid="${fid}"]`);

                if (existingButton) {
                    const countElement = existingButton.querySelector('.filter-button-count');
                    const countNum = section.elements.length;
                    countElement.textContent = countNum <= 99 ? countNum : '99+';

                    existingButton.classList.toggle('hidden', hiddenSections[fid]);
                } else {
                    addButton(section, fid);
                }
            });
        };

        const addButton = (section, fid) => {
            const button = document.createElement('a');
            button.className = 'filter-button';
            button.textContent = section.name;
            button.dataset.fid = fid;
            button.classList.toggle('hidden', hiddenSections[fid]);

            const countElement = document.createElement('span');
            countElement.className = 'filter-button-count';
            countElement.textContent = section.elements.length;
            button.insertBefore(countElement, button.firstChild);

            button.addEventListener('click', () => {
                hiddenSections[fid] = !hiddenSections[fid];
                section.elements.forEach(thread => {
                    thread.style.display = hiddenSections[fid] ? 'none' : '';
                });
                button.classList.toggle('hidden', hiddenSections[fid]);
                updateHiddenSections();
            });

            filterContainer.appendChild(button);
        };

        const filterContainer = document.createElement('div');
        filterContainer.className = 'filter-container';
        document.body.appendChild(filterContainer);

        updateFilterButtons();

        let isLoading = false;
        let lastLoadedTime = Date.now();

        const loadNextPage = () => {
            if (isLoading) return;

            const nextPageLink = document.querySelector('.pg a.nxt');
            if (!nextPageLink) return;

            isLoading = true;
            const loadingIndicator = createLoadingIndicator('加载中...');
            document.body.appendChild(loadingIndicator);

            const loadTime = 3000;
            const elapsedTime = Date.now() - lastLoadedTime;
            const waitTime = Math.max(0, loadTime - elapsedTime);

            setTimeout(() => {
                fetch(nextPageLink.href)
                    .then(response => response.text())
                    .then(html => {
                        const parser = new DOMParser();
                        const doc = parser.parseFromString(html, 'text/html');

                        const errorMessage = doc.querySelector('#messagetext.alert_error');
                        if (errorMessage && errorMessage.textContent.includes('刷新过于频繁')) {
                            showRetryIndicator();
                            setTimeout(loadNextPage, 3000);
                            return;
                        }

                        const newThreads = doc.querySelectorAll('#threadlist .pbw');
                        newThreads.forEach(thread => {
                            thread.classList.add('fade-in'); // 添加渐入动效类
                            document.querySelector('#threadlist ul').appendChild(thread);
                            processThread(thread);
                        });

                        applyFilterToNewThreads(newThreads);

                        const newPagination = doc.querySelector('.pgs.cl.mbm');
                        const pagination = document.querySelector('.pgs.cl.mbm');
                        if (pagination && newPagination) {
                            pagination.replaceWith(newPagination);
                        }

                        updateFilterButtons();
                        loadingIndicator.remove();
                        lastLoadedTime = Date.now();
                    }).finally(() => {
                        isLoading = false;
                    });
            }, waitTime);
        };

        const showRetryIndicator = () => {
            const retryIndicator = createLoadingIndicator('翻页过快,即将重试...');
            document.body.appendChild(retryIndicator);

            setTimeout(() => {
                retryIndicator.remove();
                loadNextPage();
            }, 3000);
        };

        const loadNextPageIfNeeded = () => {
            const scrollPosition = window.innerHeight + window.scrollY;
            const threshold = document.documentElement.scrollHeight - window.innerHeight / 2;

            if (scrollPosition >= threshold && !isLoading) {
                loadNextPage();
            }
        };

        window.addEventListener('wheel', (event) => {
            if (event.deltaY > 0) {
                loadNextPageIfNeeded();
            }
        });

        window.addEventListener('keydown', (event) => {
            if (event.code === 'Space') {
                loadNextPageIfNeeded();
            }
        });

        const style = document.createElement('style');
        style.textContent = `
        .filter-container {
            position: fixed;
            right: 10px;
            top: 140px;
            display: flex;
            flex-direction: column;
            z-index: 9999;
            width: 140px;
        }

        .filter-button {
            color: #333;
            border: none;
            padding: 1px 2px;
            box-shadow: 2px 2px 1px 0 #0009;
            white-space: pre;
            font-size: 14px;
            line-height: 18px;
            margin: 8px 0;
            cursor: pointer;
            outline: 2px solid lightblue;
            font-weight: bold;
            background: #EEEE;
            position: relative;
            padding-left: 32px;
        }

        .filter-button.hidden {
            color: #CCC;
            background: #999C;
            text-decoration: line-through;
            outline-color: #999;
        }

        .filter-button:hover {
            outline-color: deepskyblue;
        }
        .filter-button-count {
            color: royalblue;
            padding-right: 10px;
            position: absolute;
            left: 4px;
            transition: transform 0.3s ease, color 0.3s ease;
        }

        .hidden .filter-button-count {
            color: #CCC;
        }

        @keyframes fadeIn {
            from {
                opacity: 0;
            }
            to {
                opacity: 1;
            }
        }

        .fade-in {
            opacity: 0;
            animation: fadeIn 1s forwards;
        }

        .loading-indicator {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background-color: rgba(0, 0, 0, 0.8);
            color: #fff;
            padding: 12px 18px;
            border-radius: 5px;
            font-size: 14px;
            font-weight: bold;
            box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.4);
            z-index: 10000;
            display: flex;
            align-items: center;
            gap: 8px;
        }
        .loading-indicator::before {
            content: '';
            display: inline-block;
            width: 14px;
            height: 14px;
            border: 3px solid #fff;
            border-radius: 50%;
            border-top-color: transparent;
            animation: spin 1s linear infinite;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        `;

        document.head.appendChild(style);
    }

})();