您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhance ruTorrent with dynamic color coding for torrent ratios.
// ==UserScript== // @name ruTorrent Ratio Color // @namespace http://tampermonkey.net/ // @version 2025-09-10:3 // @description Enhance ruTorrent with dynamic color coding for torrent ratios. // @author razorwax // @match *://*/*rutorrent/ // @license MIT // ==/UserScript== const config = { debug: false, maxRatio: 10.0, selectors: { ratioElem: ".stable-List-col-6", ratioParent: "div.stable-body tbody:not([class])", parentContainer: "div#List" } }; let ratioElemIdentifier = config.selectors.ratioElem; let ratioParentElemIdentifier = config.selectors.ratioParent; let parentContainerElemIdentifier = config.selectors.parentContainer; let alertCount = 0; function TriggerError(msg) { const taggedMsg = "[DDRatiocolor] Error: " + msg; console.error(taggedMsg); if (alertCount < 1) { alert(taggedMsg); alertCount++; } else { console.warn(taggedMsg); } } const TriggerLog = msg => { if (config.debug) console.log(`[DDRatiocolor] Log: ${msg}`); }; function Clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function MapRange(value, inputMin, inputMax, outputMin = 0.0, outputMax = 1.0) { outputMin = typeof outputMin === 'number' ? outputMin : 0.0; outputMax = typeof outputMax === 'number' ? outputMax : 1.0; if (inputMin === inputMax) { return outputMin; } if (inputMin > inputMax) { [inputMin, inputMax] = [inputMax, inputMin]; } let flippedOutput = false; if (outputMin > outputMax) { flippedOutput = true; [outputMin, outputMax] = [outputMax, outputMin]; } let mappedValue = outputMin + ((value - inputMin) / (inputMax - inputMin)) * (outputMax - outputMin); if (flippedOutput) { mappedValue = outputMin + outputMax - mappedValue; } return Clamp(mappedValue, outputMin, outputMax); } // Interpolate between two colors depending on the supplied factor, it also adds an extra effect on the final color to make them match better with each other function InterpolateColorWithEffect(color1, color2, factor) { factor = Clamp(factor, 0, 1); const r1 = parseInt(color1.substring(1, 3), 16); const g1 = parseInt(color1.substring(3, 5), 16); const b1 = parseInt(color1.substring(5, 7), 16); const r2 = parseInt(color2.substring(1, 3), 16); const g2 = parseInt(color2.substring(3, 5), 16); const b2 = parseInt(color2.substring(5, 7), 16); const neutralFactor = 0.5; const addColor = neutralFactor * 200.0; const darkenFactor = MapRange(neutralFactor, 0.0, 1.0, 1.0, 0.5); const r = Math.round(Clamp((r1 + (r2 - r1) * factor + addColor) * darkenFactor, 0.0, 255.0)); const g = Math.round(Clamp((g1 + (g2 - g1) * factor + addColor) * darkenFactor, 0.0, 255.0)); const b = Math.round(Clamp((b1 + (b2 - b1) * factor + addColor) * darkenFactor, 0.0, 255.0)); var hex = '#' + (r << 16 | g << 8 | b).toString(16).padStart(6, '0'); return hex; } function SetColorOnRatioElem(ratioElem) { if (!ratioElem) { TriggerError("SetColorOnRatioElem was called without correct elem!"); return; } const ratio = parseFloat(ratioElem.textContent); if (isNaN(ratio)) { TriggerError("SetColorOnRatioElem was called with a ratio element that was not converted to a valid number: " + ratioElem.textContent); return; } const clampedRatio = Math.min(ratio, config.maxRatio); const applyBackground = function(bg) { ratioElem.style.setProperty("--ddr-bg", bg, "important"); ratioElem.classList.add("ddr-bg"); const parentTd = ratioElem.closest("td"); if (parentTd) { parentTd.style.setProperty("--ddr-bg", bg, "important"); parentTd.classList.add("ddr-bg"); } }; if (clampedRatio < 0.5) { applyBackground(InterpolateColorWithEffect("#ff0000", "#ff8000", MapRange(clampedRatio, 0.0, 0.5))); } else if (clampedRatio < 1.0) { applyBackground(InterpolateColorWithEffect("#ff8000", "#e0ff00", MapRange(clampedRatio, 0.5, 1.0))); } else if (clampedRatio < 5.0) { applyBackground(InterpolateColorWithEffect("#00ff00", "#00e0e0", MapRange(clampedRatio, 1.0, 5.0))); } else { // For ratios above 5.0 up to maxRatio, interpolate to the max color; above maxRatio, stays at the max color applyBackground(InterpolateColorWithEffect("#00e0e0", "#8040ff", MapRange(clampedRatio, 5.0, config.maxRatio))); } } const colorTrackingObservers = new WeakMap(); const colorTrackingObservedElems = new Set(); function AddColorTracking(ratioElem) { // If an observer already exists for this element, replace it to avoid duplicates const existing = colorTrackingObservers.get(ratioElem); if (existing) { existing.disconnect(); colorTrackingObservers.delete(ratioElem); } const updateCallback = function() { SetColorOnRatioElem(ratioElem); }; const observer = new MutationObserver(updateCallback); const config = { characterData: true, childList: true, subtree: true }; observer.observe(ratioElem, config); colorTrackingObservers.set(ratioElem, observer); colorTrackingObservedElems.add(ratioElem); } function RemoveColorTracking(ratioElem) { // Look up the observer for the given ratio element. const observer = colorTrackingObservers.get(ratioElem); if (observer) { observer.disconnect(); colorTrackingObservers.delete(ratioElem); colorTrackingObservedElems.delete(ratioElem); } } let foundParentElement = false; let ranDetectOnFirstFind = false; function FoundParentElement(parentElem) { if (foundParentElement) { TriggerError("Parent element was already found, but FoundParentElement was called multiple times!"); return; } foundParentElement = true; TriggerLog("Found parent element, starting setting color per ratio element!"); const findRatioElement = function(elem, callback) { if (!ranDetectOnFirstFind) { DetectRatioSelector(); ranDetectOnFirstFind = true; } const ratioElem = elem.matches(ratioElemIdentifier) ? elem : elem.querySelector(ratioElemIdentifier); if (ratioElem) { callback(ratioElem); } }; const updateCallback = function(mutationsList, observer) { for (const mutation of mutationsList) { mutation.addedNodes.forEach(addedElem => { if (addedElem.nodeType !== Node.ELEMENT_NODE) { return; } findRatioElement(addedElem, ratioElem => { SetColorOnRatioElem(ratioElem); AddColorTracking(ratioElem); }); }); mutation.removedNodes.forEach(removedElem => { if (removedElem.nodeType !== Node.ELEMENT_NODE) { return; } findRatioElement(removedElem, ratioElem => { RemoveColorTracking(ratioElem); }); }); } }; const observer = new MutationObserver(updateCallback); const config = { childList: true }; observer.observe(parentElem, config); const ratioElems = parentElem.querySelectorAll(ratioElemIdentifier); if (ratioElems) { ratioElems.forEach(function(elem) { SetColorOnRatioElem(elem); AddColorTracking(elem); }); } } function WaitForParentElement() { TriggerLog("Parent element doesn't exist yet, wait for it to be created"); const parentContainerElem = document.querySelector(parentContainerElemIdentifier); if (!parentContainerElem) { TriggerError("Couldn't find container for parent element, something is very wrong!"); return; } const updateCallback = function(mutationsList, observer) { for (let mutation of mutationsList) { if (foundParentElement) { observer.disconnect(); return; } mutation.addedNodes.forEach(function(addedElem) { if (!foundParentElement && addedElem.nodeType === 1) { var ratioParentElem = addedElem.querySelector(ratioParentElemIdentifier); if (ratioParentElem) { FoundParentElement(ratioParentElem); return; } } }); } }; const observer = new MutationObserver(updateCallback); const config = { childList: true, subtree: true }; observer.observe(parentContainerElem, config); } function FindParentElement() { const findParentRatioElemStr = parentContainerElemIdentifier + " " + ratioParentElemIdentifier; const parentElem = document.querySelectorAll(findParentRatioElemStr); if (!parentElem || parentElem.length == 0) { WaitForParentElement(); return; } if (parentElem.length > 1) { TriggerError("Couldn't find parent element in a safe way, taking a guess"); } FoundParentElement(parentElem[0]); } function EnsureStyleInjected() { const styleId = "ddr-style"; if (document.getElementById(styleId)) { return; } const style = document.createElement("style"); style.id = styleId; style.textContent = ".ddr-bg{background-color:var(--ddr-bg) !important;color:#000 !important;}" + "td.ddr-bg{background:var(--ddr-bg) !important;}"; document.head.appendChild(style); } function DetectRatioSelector() { try { const tbody = document.querySelector(parentContainerElemIdentifier + " " + ratioParentElemIdentifier); if (!tbody) { TriggerLog("DetectRatioSelector: tbody not found yet"); return; } const table = tbody.closest("table"); if (!table) { TriggerLog("DetectRatioSelector: table not found from tbody"); return; } const headerRow = table.querySelector("thead tr"); if (!headerRow) { TriggerLog("DetectRatioSelector: thead/tr not found"); return; } const headerCells = headerRow.querySelectorAll("td, th"); if (!headerCells || headerCells.length === 0) { TriggerLog("DetectRatioSelector: no header cells in thead"); return; } let ratioIndex = -1; for (let i = 0; i < headerCells.length; i++) { const cell = headerCells[i]; const span = cell.querySelector('span'); const text = ((span && span.textContent) ? span.textContent : (cell.textContent || "")).trim().toLowerCase(); if (text === "ratio") { ratioIndex = i; break; } } if (ratioIndex === -1) { TriggerLog("DetectRatioSelector: 'Ratio' header not found; keeping defaults"); return; } // Build selector for the corresponding body column's inner <div> ratioElemIdentifier = parentContainerElemIdentifier + " " + ratioParentElemIdentifier + " tr > td:nth-child(" + (ratioIndex + 1) + ") > div"; TriggerLog("Auto-detected ratio selector: " + ratioElemIdentifier); } catch (e) { TriggerLog("Failed to auto-detect ratio selector: " + e.message); } } (function() { 'use strict'; try { // Do not trigger in iframes const inIframe = window.top != window.self; if (inIframe) { return; } EnsureStyleInjected(); FindParentElement(); } catch (e) { TriggerError("Unhandled initialization error: " + (e && e.message ? e.message : e)); } })();