Javdb 优质影片高亮 (排序与优化版)

为Javdb上评分高的影片增加热力图风格的高亮,同时支持根据热力排序。

As of 2025-06-25. See the latest version.

// ==UserScript==
// @name         Javdb 优质影片高亮 (排序与优化版)
// @name:zh      Javdb 优质影片高亮 (排序与优化版)
// @name:en      Javdb Quality Video Highlighter & Sorter
// @namespace    http://tampermonkey.net/
// @version      1.1.1
// @description  为Javdb上评分高的影片增加热力图风格的高亮,同时支持根据热力排序。
// @description:zh  为Javdb上评分高的影片增加热力图风格的高亮,同时支持根据热力排序。
// @description:en Adds a background highlight to premium videos on Javdb based on rating and view count data, while also supporting sorting by heat.
// @author       JHT,黄页大嫖客(Modified by Gemini)
// @match        https://javdb.com/*
// @grant        GM_addStyle
// @license      GPLv3
// @homeURL https://sleazyfork.org/zh-CN/scripts/539525
// @supportURL https://sleazyfork.org/zh-CN/scripts/539525/feedback
// ==/UserScript==

(function() {
    'use strict';

    // --- 配置 ---
    const SCORE_THRESHOLD = 3.75; // 评分阈值,只有高于此分数的影片才会被着色
    const MAX_RATED_BY = 200;     // 用于归一化的最大评价人数,超过此值视为满热度贡献

    /**
     * 根据综合热度值计算热力图颜色 (蓝 -> 绿 -> 红)。
     * @param {number} heat - 一个 0 到 1 之间的值,代表热度。
     * @returns {string} CSS rgba 颜色字符串,透明度固定为 50%。
     */
    function getHeatmapColor(heat) {
        let r = 0, g = 0, b = 0;
        const h = Math.min(Math.max(heat, 0), 1);

        if (h < 0.5) {
            const t = h * 2;
            g = Math.round(255 * t);
            b = Math.round(255 * (1 - t));
        } else {
            const t = (h - 0.5) * 2;
            r = Math.round(255 * t);
            g = Math.round(255 * (1 - t));
        }
        return `rgba(${r}, ${g}, ${b}, 0.5)`;
    }

    /**
     * 根据影片评分和评价人数计算综合热度值。
     * @param {number} score - 影片评分。
     * @param {number} ratedBy - 评价人数。
     * @returns {number} 一个 0 到 1 之间的热度值。
     */
    function calculateHeat(score, ratedBy) {
        const scoreNormalized = score / 5;
        const scoreTransformed = Math.pow(scoreNormalized, 2);
        const ratedByNormalized = Math.min(ratedBy / MAX_RATED_BY, 1);
        return scoreTransformed * ratedByNormalized;
    }

    const scoreRegex = /([\d.]+)[^\d]+(\d+)/;

    /**
     * 计算热度并将其存储在元素上,然后应用高亮。
     * @param {HTMLElement} item - 影片的 item 元素。
     */
    function processAndHighlightItem(item) {
        if (item.dataset.highlighted) return;

        const scoreElement = item.querySelector('.score .value');
        const anchorElement = item.querySelector('a');
        item.dataset.heat = '0';

        if (scoreElement && anchorElement) {
            const scoreText = scoreElement.textContent.trim();
            const scoreMatch = scoreText.match(scoreRegex);

            if (scoreMatch) {
                const score = parseFloat(scoreMatch[1]);
                if (score >= SCORE_THRESHOLD) {
                    const ratedBy = parseInt(scoreMatch[2], 10);
                    const heat = calculateHeat(score, ratedBy);
                    item.dataset.heat = heat;
                    anchorElement.style.backgroundColor = getHeatmapColor(heat);
                }
            }
        }
        item.dataset.highlighted = 'true';
    }

    /**
     * 在指定的根元素下查找并处理所有影片项。
     * @param {HTMLElement} rootElement - 开始搜索 .item 的根元素。
     */
    function processItemsIn(rootElement) {
        const lists = rootElement.matches('.movie-list') ? [rootElement] : rootElement.querySelectorAll('.movie-list');
        lists.forEach(list => {
            list.querySelectorAll('.item').forEach(processAndHighlightItem);
        });
    }

    /**
     * 创建一个独立的、浮动的排序按钮。
     */
    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 = navigator.language.startsWith('zh') ? '排序' : 'Sort';
        sortButton.className = 'button';

        sortButton.addEventListener('click', (event) => {
            event.preventDefault();
            sortItemsByHeat();
        });

        container.appendChild(sortButton);
        document.body.appendChild(container);

        GM_addStyle(`
            #sort-by-heat-btn-container {
                position: fixed;
                bottom: 27.2px;
                right: 0px;
                z-index: 9998;
            }
            #sort-by-heat-btn-container .button {
                width: 39.3px;
                height: 27.2px;
                font-size: 0.8rem;
                background-color: #fa6699;
                color: white;
                border: none;
                padding: 0;
                display: flex;
                align-items: center;
                justify-content: center;
                cursor: pointer;
            }
        `);
    }

    /**
     * 对页面上所有 movie-list 中的项目进行统一排序。
     */
    function sortItemsByHeat() {
        const containers = document.querySelectorAll('.movie-list');
        if (containers.length === 0) return;

        let allItems = [];
        containers.forEach(container => {
            allItems = allItems.concat(Array.from(container.querySelectorAll('.item')));
        });

        allItems.sort((a, b) => {
            const heatA = parseFloat(a.dataset.heat || 0);
            const heatB = parseFloat(b.dataset.heat || 0);
            return heatB - heatA;
        });

        const primaryContainer = containers[0];
        const fragment = document.createDocumentFragment();
        allItems.forEach(item => fragment.appendChild(item));

        primaryContainer.innerHTML = '';
        primaryContainer.appendChild(fragment);

        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;

        const elementTopPosition = primaryContainer.getBoundingClientRect().top + window.pageYOffset;
        const targetScrollPosition = elementTopPosition - offset;

        window.scrollTo({
            top: targetScrollPosition,
            behavior: 'smooth'
        });
    }

    /**
     * 脚本主函数/入口
     */
    function main() {
        try {
            processItemsIn(document.body);
            createIndependentSortButton();

            const observer = new MutationObserver((mutationsList) => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList' && mutation.addedNodes.length) {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === 1 && (node.matches('.movie-list') || node.querySelector('.movie-list'))) {
                                processItemsIn(node);
                                createIndependentSortButton();
                            }
                        });
                    }
                }
            });
            observer.observe(document.body, { childList: true, subtree: true });
        } catch (error) {
            console.error("Javdb Highlighter & Sorter script error:", error);
        }
    }

    // --- 脚本执行 ---
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }

})();