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

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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);

})();