您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
增强 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">×</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(); } })();