Kemono 更新標示 (虛擬DOM兼容版)

Kemono更新標示,支援虛擬DOM。導航時強制重新處理。

// ==UserScript==
// @name              Kemono 更新標示 (虛擬DOM兼容版)
// @name:zh-TW        Kemono 更新標示 (虛擬DOM兼容版)
// @name:zh-CN        Kemono 更新标记 (虚拟DOM兼容版)
// @namespace         https://greasyfork.org/zh-CN/users/1051751-mark-levi
// @version           2.4.0
// @description       Kemono post highlighter with virtual DOM support. Force reprocessing on navigation.
// @description:zh-TW Kemono更新標示,支援虛擬DOM。導航時強制重新處理。
// @description:zh-CN Kemono更新标记,支援虚拟DOM。导航时强制重新处理。
// @author            Your Name Here
// @match             https://kemono.cr/*
// @match             http://kemono.cr/*
// @match             https://kemono.su/*
// @match             http://kemono.su/*
// @grant             none
// @run-at            document-idle
// @license           MIT
// ==/UserScript==

(function() {
    'use strict';

    const prefix = '[KemonoCR]';
    const log = (...args) => console.log(prefix, ...args);
    let isProcessing = false;

    // --- 日期處理函式 ---
    const toYMD = (date) => {
        const y = date.getFullYear();
        const m = String(date.getMonth() + 1).padStart(2, '0');
        const d = String(date.getDate()).padStart(2, '0');
        return `${y}-${m}-${d}`;
    };

    const today = new Date();
    const todayStr = toYMD(today);

    // 計算天數差的函式
    function getDayDiff(dateStr) {
        try {
            const postDate = new Date(dateStr);
            const todayNoTime = new Date(todayStr);
            const postDateNoTime = new Date(dateStr);

            const diffTime = todayNoTime - postDateNoTime;
            const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
            return Math.max(0, diffDays);
        } catch (error) {
            return -1;
        }
    }

    /**
     * 添加標記到元素
     */
    function appendLabel(el, text, dayDiff) {
        // 移除現有的標記(如果存在)
        const existingLabel = el.querySelector('.kemono-label');
        if (existingLabel) {
            existingLabel.remove();
        }

        const labelSpan = document.createElement('span');
        labelSpan.className = 'kemono-label';
        labelSpan.textContent = text;
        labelSpan.style.fontWeight = 'bold';
        labelSpan.style.marginLeft = '5px';

        if (dayDiff === 0) {
            labelSpan.style.color = 'red';
        } else if (dayDiff === 1) {
            labelSpan.style.color = 'orange';
        } else if (dayDiff === 2) {
            labelSpan.style.color = 'gold';
        } else if (dayDiff === 3) {
            labelSpan.style.color = 'green';
        } else if (dayDiff === 4) {
            labelSpan.style.color = 'blue';
        } else if (dayDiff === 5) {
            labelSpan.style.color = 'purple';
        } else if (dayDiff >= 6) {
            labelSpan.style.color = '#666666';
        }

        el.appendChild(labelSpan);
    }

    /**
     * 處理單一 <time> 元素(強制重新處理)
     */
    function processElement(el) {
        // 清除處理標記,強制重新處理
        delete el.dataset.kemonoProcessed;

        const dateText = (el.textContent || '').trim().split(' ')[0];
        const dayDiff = getDayDiff(dateText);

        if (dayDiff >= 0) {
            if (dayDiff === 0) {
                appendLabel(el, '今日更新', dayDiff);
            } else {
                appendLabel(el, `${dayDiff}天前`, dayDiff);
            }
        }

        el.dataset.kemonoProcessed = 'true';
    }

    /**
     * 強制重新處理所有元素
     */
    function forceReprocessAll() {
        if (isProcessing) return;
        isProcessing = true;

        log('Force reprocessing all elements...');

        // 移除所有現有標記
        document.querySelectorAll('.kemono-label').forEach(label => {
            label.remove();
        });

        // 清除所有處理標記
        document.querySelectorAll('time.timestamp[data-kemono-processed]').forEach(el => {
            delete el.dataset.kemonoProcessed;
        });

        // 重新處理所有元素
        const elements = document.querySelectorAll('time.timestamp');
        if (elements.length > 0) {
            log(`Reprocessing ${elements.length} elements`);
            elements.forEach(processElement);
        }

        isProcessing = false;
        log('Force reprocessing complete');
    }

    /**
     * 處理新元素
     */
    function processNewElements() {
        const elements = document.querySelectorAll('time.timestamp:not([data-kemono-processed])');
        if (elements.length > 0) {
            log(`Processing ${elements.length} new elements`);
            elements.forEach(processElement);
        }
    }

    // --- 虛擬DOM偵測與處理 ---

    // 1. 強力定時器(主要解決方案)
    setInterval(() => {
        processNewElements();
    }, 1000);

    // 2. 監聽所有可能的變化
    const observer = new MutationObserver((mutations) => {
        let shouldReprocess = false;

        mutations.forEach(mutation => {
            // 如果有任何節點變化,就重新處理
            if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
                shouldReprocess = true;
            }

            // 如果是屬性變化,檢查是否是內容相關的
            if (mutation.type === 'attributes') {
                if (mutation.attributeName === 'class' ||
                    mutation.attributeName === 'style' ||
                    mutation.attributeName.includes('data')) {
                    shouldReprocess = true;
                }
            }
        });

        if (shouldReprocess) {
            setTimeout(processNewElements, 100);
        }
    });

    // 監聽整個文檔
    observer.observe(document.documentElement, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['class', 'style', 'data-*']
    });

    // 3. 監聽頁面焦點變化(切換標籤頁返回時)
    let lastVisibilityState = document.visibilityState;
    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'visible' && lastVisibilityState === 'hidden') {
            setTimeout(forceReprocessAll, 300);
        }
        lastVisibilityState = document.visibilityState;
    });

    // 4. 監聽滾動事件(無限滾動)
    let scrollTimeout;
    window.addEventListener('scroll', () => {
        clearTimeout(scrollTimeout);
        scrollTimeout = setTimeout(() => {
            processNewElements();
        }, 500);
    });

    // 5. 重寫 History API(SPA 導航)
    const originalMethods = {
        pushState: history.pushState,
        replaceState: history.replaceState
    };

    history.pushState = function(...args) {
        const result = originalMethods.pushState.apply(this, args);
        setTimeout(forceReprocessAll, 200);
        return result;
    };

    history.replaceState = function(...args) {
        const result = originalMethods.replaceState.apply(this, args);
        setTimeout(forceReprocessAll, 200);
        return result;
    };

    window.addEventListener('popstate', () => {
        setTimeout(forceReprocessAll, 300);
    });

    // 6. 監聽點擊事件(分頁按鈕)
    document.addEventListener('click', (event) => {
        const target = event.target;
        // 檢查是否是分頁相關的元素
        if (target.closest('.pagination, [data-page], [href*="page="]')) {
            setTimeout(forceReprocessAll, 500);
        }
    });

    // 7. 初始處理
    setTimeout(() => {
        forceReprocessAll();
        log('Virtual DOM compatible script loaded');
    }, 1000);

})();