Javdb 增强脚本

增强 Javdb 浏览体验,提供多种可配置功能:热门影片热力图高亮、热度排序、以及在列表页直接管理"已看"/"想看"状态。兼容自动翻页脚本。

// ==UserScript==
// @name         Javdb 增强脚本
// @name:zh      Javdb 增强脚本
// @name:en      Javdb Enhanced Script
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @icon         https://javdb.com/favicon-32x32.png
// @description  增强 Javdb 浏览体验,提供多种可配置功能:热门影片热力图高亮、热度排序、以及在列表页直接管理"已看"/"想看"状态。兼容自动翻页脚本。
// @description:zh  增强 Javdb 浏览体验,提供多种可配置功能:热门影片热力图高亮、热度排序、以及在列表页直接管理"已看"/"想看"状态。兼容自动翻页脚本。
// @description:en  Enhances Javdb with configurable features: heatmap highlighting for popular videos, sorting by heat, and direct status management (Watched/Want) on list pages. Compatible with auto-paging scripts.
// @author       JHT, 黄页大嫖客 (Modified by Gemini)
// @match        https://javdb*.com/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      javdb*.com
// @license      GPLv3
// @homeURL      https://sleazyfork.org/zh-CN/scripts/539525
// @supportURL   https://sleazyfork.org/zh-CN/scripts/539525/feedback
// ==/UserScript==

(function() {
    'use strict';

    // --- I18N 国际化模块 ---
    const I18N = {
        zh: {
            settings: '设置',
            highlightFeature: '启用高亮功能',
            sortFeature: '启用排序功能',
            statusFeature: '启用状态标记功能',
            sort: '排序',
            unmarked: '暂未标记',
            want: '想看',
            watched: '已看',
            modify: '修改',
            delete: '删除',
            confirmDelete: '确认删除标记?',
            confirm: '确认',
            cancel: '取消'
        },
        en: {
            settings: 'Settings',
            highlightFeature: 'Enable Highlight Feature',
            sortFeature: 'Enable Sort Feature',
            statusFeature: 'Enable Status Tag Feature',
            sort: 'Sort',
            unmarked: 'Unmarked',
            want: 'Want',
            watched: 'Watched',
            modify: 'Modify',
            delete: 'Delete',
            confirmDelete: 'Confirm Delete?',
            confirm: 'Confirm',
            cancel: 'Cancel'
        }
    };

    const getBrowserLanguage = () => {
        const browserLang = navigator.language || navigator.userLanguage || 'zh';
        return browserLang.toLowerCase().startsWith('zh') ? 'zh' : 'en';
    };
    const lang = getBrowserLanguage();
    const T = (key) => I18N[lang][key] || I18N['zh'][key];

    // --- 配置与全局变量 ---
    const SETTINGS_KEY = 'JavdbEnhanced_Settings';
    let settings = {
        highlight: true,
        sort: true,
        status: true,
        highlightThreshold: 3.75,
        ratedByThreshold: 200,
        fetchDelay: 300
    };
    let authenticityToken = null;
    const scoreRegex = /([\d.]+)[^\d]+(\d+)/;

    // --- 样式注入 ---
    GM_addStyle(`
        :root {
            --rem-base: 16px;
            --color-sort-button: #fa6699;
            --color-unmarked: rgba(100, 100, 100, 0.9);
            --color-watched: #3273dc;
            --color-wanted: #e83e8c;
            --color-delete: #ff3860;
            --color-modify: #ffc107;
            --color-modify-hover-text: #212529;
            --color-star: #ccc;
            --color-star-filled: #ffdd44;
        }
        #sort-by-heat-btn-container { position: fixed; bottom: 1.7rem; right: 0; z-index: 9998; }
        #sort-by-heat-btn-container .button { width: 2.45rem; height: 1.7rem; font-size: 0.8rem; background-color: var(--color-sort-button); color: white; border: none; padding: 0; display: flex; align-items: center; justify-content: center; cursor: pointer; }
        .item .cover { position: relative; overflow: hidden; }
        .item .cover img.cover-img-blurred { filter: blur(0.25rem) brightness(0.8); transform: scale(1.05); transition: all 0.3s ease; }
        .cover-status-buttons { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 5; display: flex; flex-direction: column; align-items: center; width: 100%; }
        .cover-status-buttons .state { display: none; flex-direction: column; align-items: center; width: 100%; gap: 0.5rem; }
        .cover-status-buttons.show-unmarked .state-unmarked, .cover-status-buttons.show-watched .state-watched, .cover-status-buttons.show-wanted .state-wanted { display: flex; }
        .status-tag-main { width: 5rem; height: 2rem; border-radius: 0.3125rem; display: flex; justify-content: center; align-items: center; color: white; font-weight: bold; font-size: 0.9rem; cursor: pointer; }
        .state-unmarked .status-tag-main { background-color: var(--color-unmarked); }
        .state-watched .status-tag-main { background-color: var(--color-watched); }
        .state-wanted .status-tag-main { background-color: var(--color-wanted); }
        .action-buttons { display: flex; gap: 0.5rem; }
        .action-buttons .button { padding: 0.25rem 0.625rem; font-size: 0.8rem; border: none; border-radius: 0.25rem; color: white; cursor: pointer; transition: background-color 0.2s ease; }
        .btn-set-wanted { background-color: var(--color-wanted); }
        .btn-set-watched { background-color: var(--color-watched); }
        .btn-modify { background-color: var(--color-modify); }
        .btn-delete { background-color: var(--color-delete); }
        .btn-set-wanted:hover { background-color: var(--color-wanted); }
        .btn-set-watched:hover { background-color: var(--color-watched); }
        .btn-modify:hover { background-color: var(--color-modify); color: var(--color-modify-hover-text); }
        .btn-delete:hover { background-color: var(--color-delete); }
        .action-buttons-hover { display: none; }
        .state-watched:hover .action-buttons-hover { display: flex; }
        .state-watched:hover .user-rating-display { display: none; }
        .user-rating-display { display: none; }
        .user-rating-display.is-visible { display: flex; }
        .user-rating-display span { font-size: 1rem; color: var(--color-star); padding: 0 0.0625rem; text-shadow: 0 0 0.1875rem black; }
        .user-rating-display span.is-filled { color: var(--color-star-filled); }
        .jdbe-modal, .cover-modal-base { display: flex; justify-content: center; align-items: center; }
        .jdbe-modal { position: fixed; z-index: 10000; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
        .jdbe-modal-content { background: #fff; padding: 1.25rem; border-radius: 0.5rem; box-shadow: 0 0.3125rem 1rem rgba(0,0,0,0.3); }
        .jdbe-modal-close { float: right; cursor: pointer; border: none; background: none; font-size: 1.5rem; }
        .jdbe-modal-body { margin-top: 1.25rem; }
        .jdbe-modal-body .setting-row { margin-bottom: 0.625rem; display: flex; align-items: center; }
        .jdbe-modal-body label { margin-left: 0.5rem; }
        .cover-modal-base { position: absolute; z-index: 10; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); backdrop-filter: blur(0.125rem); flex-direction: column; gap: 0.625rem; }
        .rating-modal-container { z-index: 15; }
        .rating-stars { display: flex; flex-direction: row-reverse; }
        .rating-stars input[type="radio"] { display: none; }
        .rating-stars label { font-size: 1.5rem; color: var(--color-star); cursor: pointer; transition: color 0.2s; }
        .rating-stars label:hover, .rating-stars label:hover ~ label, .rating-stars input[type="radio"]:checked ~ label { color: var(--color-star-filled); }
        .rating-modal-container .btn-cancel-rating { background-color: #f5f5f5; padding: 0.25rem 0.5rem; font-size: 0.7rem; border-radius: 0.25rem; border: none; cursor: pointer; margin-top: 0.3125rem;}
        .confirm-delete-modal p { color: white; font-weight: bold; }
        .confirm-delete-modal .buttons { display: flex; gap: 0.625rem; }
        .confirm-delete-modal .button { padding: 0.3125rem 0.75rem; border-radius: 0.25rem; border: none; cursor: pointer; }
        .confirm-delete-modal .is-danger { background-color: var(--color-delete); color: white; }
        .confirm-delete-modal .is-light { background-color: #f5f5f5; }

        body.jdbe-highlight-disabled .item a.box {
            background-color: transparent !important;
        }
        body.jdbe-status-disabled .cover-status-buttons {
            display: none !important;
        }
        body.jdbe-status-disabled .cover img.cover-img-blurred {
            filter: none !important;
            transform: none !important;
        }
    `);

    // --- 设置与 UI 更新模块 ---

    function loadSettings() {
        const savedSettings = GM_getValue(SETTINGS_KEY, settings);
        settings = { ...settings, ...savedSettings };
    }

    function saveSettings() {
        GM_setValue(SETTINGS_KEY, settings);
    }

    function updateUIVisibility() {
        document.body.classList.toggle('jdbe-highlight-disabled', !settings.highlight);
        document.body.classList.toggle('jdbe-status-disabled', !settings.status);

        const sortButton = document.getElementById('sort-by-heat-btn-container');
        if (sortButton) {
            sortButton.style.display = (settings.sort && settings.highlight) ? 'block' : 'none';
        }

        document.querySelectorAll('.item').forEach(item => {
            const anchorElement = item.querySelector('a.box');
            if (anchorElement) {
                anchorElement.style.backgroundColor = (settings.highlight && item.dataset.heatColor) ? item.dataset.heatColor : '';
            }
        });
    }

    function openSettingsModal() {
        let modal = document.getElementById('jdbe-settings-modal');
        if (modal) {
            modal.style.display = 'flex';
            return;
        }

        modal = document.createElement('div');
        modal.id = 'jdbe-settings-modal';
        modal.className = 'jdbe-modal';
        modal.innerHTML = `
            <div class="jdbe-modal-content">
                <button class="jdbe-modal-close">&times;</button>
                <h2>${T('settings')}</h2>
                <div class="jdbe-modal-body">
                    <div class="setting-row">
                        <input type="checkbox" id="setting-highlight">
                        <label for="setting-highlight">${T('highlightFeature')}</label>
                    </div>
                    <div class="setting-row">
                        <input type="checkbox" id="setting-sort">
                        <label for="setting-sort">${T('sortFeature')}</label>
                    </div>
                    <div class="setting-row">
                        <input type="checkbox" id="setting-status">
                        <label for="setting-status">${T('statusFeature')}</label>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(modal);

        const closeModal = () => modal.style.display = 'none';
        modal.querySelector('.jdbe-modal-close').addEventListener('click', closeModal);
        modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });

        const highlightCheck = document.getElementById('setting-highlight');
        const sortCheck = document.getElementById('setting-sort');
        const statusCheck = document.getElementById('setting-status');

        highlightCheck.checked = settings.highlight;
        sortCheck.checked = settings.sort;
        statusCheck.checked = settings.status;

        const updateSortDependency = () => {
            sortCheck.disabled = !highlightCheck.checked;
            if (!highlightCheck.checked) {
                sortCheck.checked = false;
            }
        };
        updateSortDependency();

        highlightCheck.addEventListener('change', (e) => {
            settings.highlight = e.target.checked;
            updateSortDependency();
            if (!settings.highlight) {
                settings.sort = false;
            }
            updateUIVisibility();
            saveSettings();
        });

        sortCheck.addEventListener('change', (e) => {
            settings.sort = e.target.checked;
            updateUIVisibility();
            saveSettings();
        });

        statusCheck.addEventListener('change', (e) => {
            settings.status = e.target.checked;
            updateUIVisibility();
            saveSettings();
        });
    }


    // --- 核心功能 ---

    const STATUS_BUTTONS_TEMPLATE = `
        <div class="state state-unmarked">
            <div class="status-tag-main">${T('unmarked')}</div>
            <div class="action-buttons">
                <button class="button btn-set-wanted">${T('want')}</button>
                <button class="button btn-set-watched js-set-watched">${T('watched')}</button>
            </div>
        </div>
        <div class="state state-watched">
            <div class="status-tag-main">${T('watched')}</div>
            <div class="bottom-content-area">
                <div class="user-rating-display">
                    <span>★</span><span>★</span><span>★</span><span>★</span><span>★</span>
                </div>
                 <div class="action-buttons-hover action-buttons">
                     <button class="button btn-modify">${T('modify')}</button>
                     <button class="button btn-delete js-delete">${T('delete')}</button>
                </div>
            </div>
        </div>
        <div class="state state-wanted">
            <div class="status-tag-main">${T('want')}</div>
            <div class="action-buttons">
                 <button class="button btn-set-watched js-set-watched">${T('watched')}</button>
                 <button class="button btn-delete js-delete">${T('delete')}</button>
            </div>
        </div>
    `;

    function getCsrfToken() {
        const tokenMeta = document.querySelector('meta[name="csrf-token"]');
        if (tokenMeta) { authenticityToken = tokenMeta.content; }
    }

    function isLoggedIn() {
        return document.querySelector('a[href="/logout"]') !== null;
    }

    function updateReviewStatus(action, videoId, item, rating = null) {
        if (!authenticityToken) { return; }
        let urlPath, method, body;
        const reviewId = item.dataset.reviewId;

        switch (action) {
            case 'watched':
                if (rating === null) { return; }
                urlPath = reviewId ? `/v/${videoId}/reviews/${reviewId}` : `/v/${videoId}/reviews`;
                method = 'POST';
                const baseBody = `authenticity_token=${encodeURIComponent(authenticityToken)}&video_review[status]=watched&video_review[score]=${rating}&video_review[content]=`;
                body = reviewId ? `_method=patch&${baseBody}` : baseBody;
                break;
            case 'wanted':
                urlPath = `/v/${videoId}/reviews/want_to_watch`;
                method = 'POST';
                body = `authenticity_token=${encodeURIComponent(authenticityToken)}`;
                break;
            case 'delete':
                if (!reviewId) { return; }
                urlPath = `/v/${videoId}/reviews/${reviewId}`;
                method = 'POST';
                body = `_method=delete&authenticity_token=${encodeURIComponent(authenticityToken)}`;
                break;
            default: return;
        }

        const url = window.location.origin + urlPath;

        GM_xmlhttpRequest({
            method: method,
            url: url,
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
            data: body,
            onload: (response) => {
                if (response.status >= 200 && response.status < 300) {
                    fetchItemStatus(item, true);
                } else { console.error("Failed to update status:", response.status, response.responseText); }
            },
            onerror: (error) => { console.error("Error during API request:", error); }
        });
    }

    function fetchItemStatus(item, forceUpdate = false) {
        if (!settings.status) return;
        if (item.dataset.statusChecked === 'true' && !forceUpdate) return;

        const anchor = item.querySelector('a.box');
        if (!anchor) return;

        if (item.dataset.statusFetching === 'true') return;
        item.dataset.statusFetching = 'true';

        GM_xmlhttpRequest({
            method: "GET",
            url: anchor.href, // anchor.href 已经是完整的绝对路径,所以这里不需要修改
            onload: (response) => {
                if (response.status >= 200 && response.status < 300) {
                    item.dataset.statusChecked = 'true';
                    const doc = new DOMParser().parseFromString(response.responseText, "text/html");
                    setupStatusButtonsUI(doc, item);
                }
            },
            onabort: () => { item.dataset.statusFetching = 'false'; },
            onerror: () => { item.dataset.statusFetching = 'false'; },
            ontimeout: () => { item.dataset.statusFetching = 'false'; },
            onloadend: () => { item.dataset.statusFetching = 'false'; }
        });
    }

    function setupStatusButtonsUI(doc, item) {
        const buttonsContainer = item.querySelector('.cover-status-buttons');
        const coverImage = item.querySelector('.cover img');

        if (!buttonsContainer) return;

        let status = 'unmarked';
        buttonsContainer.className = 'cover-status-buttons';
        item.dataset.reviewId = '';

        const watchedTag = doc.querySelector('.review-title .tag.is-success.is-light');
        const wantedTag = doc.querySelector('.review-title .tag.is-info.is-light');
        const deleteLink = doc.querySelector('a[data-method="delete"][href*="/reviews/"]');
        const ratingDisplay = buttonsContainer.querySelector('.user-rating-display');

        if(ratingDisplay) ratingDisplay.classList.remove('is-visible');

        if (watchedTag) {
            status = 'watched';
            const ratingInput = doc.querySelector('.rating-star .control input[checked="checked"]');
            if (ratingInput && ratingDisplay) {
                const score = parseInt(ratingInput.value, 10);
                const stars = ratingDisplay.querySelectorAll('span');
                stars.forEach((star, index) => star.classList.toggle('is-filled', index < score));
                ratingDisplay.classList.add('is-visible');
            }
        } else if (wantedTag) {
            status = 'wanted';
        }

        if (deleteLink) {
            const match = deleteLink.href.match(/\/reviews\/(\d+)/);
            if (match) item.dataset.reviewId = match[1];
        }

        buttonsContainer.classList.add(`show-${status}`);

        if (coverImage) {
            coverImage.classList.add('cover-img-blurred');
        }
    }

    function showRatingModal(item, videoId) {
        const cover = item.querySelector('.cover');
        if (!cover || cover.querySelector('.rating-modal-container')) return;
        const modal = document.createElement('div');
        modal.className = 'rating-modal-container cover-modal-base';
        modal.innerHTML = `
            <div class="rating-stars">
                <input type="radio" id="star5-${videoId}" name="rating-${videoId}" value="5"><label for="star5-${videoId}">★</label>
                <input type="radio" id="star4-${videoId}" name="rating-${videoId}" value="4"><label for="star4-${videoId}">★</label>
                <input type="radio" id="star3-${videoId}" name="rating-${videoId}" value="3"><label for="star3-${videoId}">★</label>
                <input type="radio" id="star2-${videoId}" name="rating-${videoId}" value="2"><label for="star2-${videoId}">★</label>
                <input type="radio" id="star1-${videoId}" name="rating-${videoId}" value="1"><label for="star1-${videoId}">★</label>
            </div>
            <button class="btn-cancel-rating">${T('cancel')}</button>
        `;
        cover.appendChild(modal);

        modal.addEventListener('click', (e) => e.stopPropagation());

        modal.querySelectorAll('input[type="radio"]').forEach(radio => {
            radio.addEventListener('change', (e) => {
                updateReviewStatus('watched', videoId, item, e.target.value);
                cover.removeChild(modal);
            });
        });
        modal.querySelector('.btn-cancel-rating').addEventListener('click', () => {
            cover.removeChild(modal);
        });
    }

    function processItem(item) {
        if (item.dataset.enhanced) return;
        item.dataset.enhanced = 'true';

        const anchorElement = item.querySelector('a.box');
        const scoreElement = item.querySelector('.score .value');
        const coverElement = item.querySelector('.cover');

        if (!item.dataset.highlightProcessed) {
            item.dataset.highlightProcessed = 'true';
            item.dataset.heat = '0';
            if (scoreElement) {
                const scoreMatch = scoreElement.textContent.trim().match(scoreRegex);
                if (scoreMatch) {
                    const score = parseFloat(scoreMatch[1]);
                    if (score >= settings.highlightThreshold) {
                        const ratedBy = parseInt(scoreMatch[2], 10);
                        const heat = calculateHeat(score, ratedBy);
                        item.dataset.heat = heat;
                        item.dataset.heatColor = getHeatmapColor(heat);
                    }
                }
            }
        }
        if (anchorElement) {
            anchorElement.style.backgroundColor = (settings.highlight && item.dataset.heatColor) ? item.dataset.heatColor : '';
        }

        if (!item.querySelector('.cover-status-buttons')) {
            if (!coverElement || !anchorElement) return;
            const videoId = anchorElement.href.split('/').pop();
            const buttonsContainer = document.createElement('div');
            buttonsContainer.className = 'cover-status-buttons';
            buttonsContainer.innerHTML = STATUS_BUTTONS_TEMPLATE;
            coverElement.appendChild(buttonsContainer);

            const handleDeleteClick = (e) => {
                e.preventDefault(); e.stopPropagation();
                if (coverElement.querySelector('.confirm-delete-modal')) return;
                const modal = document.createElement('div');
                modal.className = 'confirm-delete-modal cover-modal-base';
                modal.innerHTML = `<p>${T('confirmDelete')}</p><div class="buttons"><button class="button is-danger btn-confirm-delete">${T('confirm')}</button><button class="button is-light btn-cancel-delete">${T('cancel')}</button></div>`;
                coverElement.appendChild(modal);
                modal.querySelector('.btn-confirm-delete').addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); updateReviewStatus('delete', videoId, item); coverElement.removeChild(modal); });
                modal.querySelector('.btn-cancel-delete').addEventListener('click', (ev) => { ev.preventDefault(); ev.stopPropagation(); coverElement.removeChild(modal); });
            };

            const handleModifyToWatched = (e) => {
                 e.preventDefault(); e.stopPropagation();
                 showRatingModal(item, videoId);
            };

            buttonsContainer.querySelector('.btn-set-wanted').addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); updateReviewStatus('wanted', videoId, item); });
            buttonsContainer.querySelectorAll('.js-set-watched, .btn-modify').forEach(btn => btn.addEventListener('click', handleModifyToWatched));
            buttonsContainer.querySelectorAll('.js-delete').forEach(btn => btn.addEventListener('click', handleDeleteClick));
        }

        if (isLoggedIn() && !item.dataset.hoverInit) {
            item.dataset.hoverInit = 'true';
            let hoverTimer;

            item.addEventListener('mouseenter', () => {
                if (settings.status) {
                    hoverTimer = setTimeout(() => fetchItemStatus(item), settings.fetchDelay);
                }
            });
            item.addEventListener('mouseleave', () => {
                clearTimeout(hoverTimer);
            });
        }
    }

    function getHeatmapColor(heat) {
        let r = 0, g = 0, b = 0;
        const h = Math.min(Math.max(heat, 0), 1);
        if (h < 0.5) {
            g = Math.round(255 * (h * 2));
            b = Math.round(255 * (1 - h * 2));
        } else {
            r = Math.round(255 * ((h - 0.5) * 2));
            g = Math.round(255 * (1 - (h - 0.5) * 2));
        }
        return `rgba(${r}, ${g}, ${b}, 0.5)`;
    }

    function calculateHeat(score, ratedBy) {
        const scoreNormalized = score / 5;
        const scoreTransformed = Math.pow(scoreNormalized, 2);
        const ratedByNormalized = Math.min(ratedBy / settings.ratedByThreshold, 1);
        return scoreTransformed * ratedByNormalized;
    }

    function createIndependentSortButton() {
        if (!document.querySelector('.movie-list') || document.getElementById('sort-by-heat-btn-container')) return;
        const container = document.createElement('div');
        container.id = 'sort-by-heat-btn-container';
        const sortButton = document.createElement('a');
        sortButton.textContent = T('sort');
        sortButton.className = 'button';
        sortButton.addEventListener('click', (e) => { e.preventDefault(); sortItemsByHeat(); });
        container.appendChild(sortButton);
        document.body.appendChild(container);
        updateUIVisibility();
    }

    function sortItemsByHeat() {
        const containers = document.querySelectorAll('.movie-list');
        if (containers.length === 0) return;
        let allItems = [];
        containers.forEach(container => { allItems.push(...container.querySelectorAll('.item')); });
        allItems.sort((a, b) => (parseFloat(b.dataset.heat || 0) - parseFloat(a.dataset.heat || 0)));
        const primaryContainer = containers[0];
        primaryContainer.innerHTML = '';
        allItems.forEach(item => primaryContainer.appendChild(item));
        for (let i = 1; i < containers.length; i++) containers[i].remove();
        const navBar = document.querySelector('.navbar.is-fixed-top');
        const offset = navBar ? navBar.getBoundingClientRect().height : 53.45;
        window.scrollTo({ top: primaryContainer.getBoundingClientRect().top + window.pageYOffset - offset, behavior: 'smooth' });
    }

    function main() {
        loadSettings();
        GM_registerMenuCommand(T('settings'), openSettingsModal);
        getCsrfToken();
        updateUIVisibility();

        try {
            document.querySelectorAll('.item').forEach(processItem);
            createIndependentSortButton();

            const observer = new MutationObserver((mutationsList) => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === Node.ELEMENT_NODE) {
                                if (node.matches('.item')) {
                                    processItem(node);
                                } else {
                                    node.querySelectorAll('.item').forEach(processItem);
                                }
                            }
                        });
                        createIndependentSortButton();
                    }
                }
            });

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

        } catch (error) {
            console.error("[Javdb Enhanced] Script execution error:", error);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }
})();