您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Auto-refreshes peer ranks every 10s, highlights your username neon cyan across the entire peer row, and adds glow for top three.
// ==UserScript== // @name TorrentBD Peer Rank // @namespace http://tampermonkey.net/ // @version 1.0 // @description Auto-refreshes peer ranks every 10s, highlights your username neon cyan across the entire peer row, and adds glow for top three. // @author gaara // @match https://*.torrentbd.net/torrents-details.php* // @grant none // @license GPL-3.0-or-later // ==/UserScript== (function() { 'use strict'; const style = document.createElement('style'); style.textContent = ` .top-peer td { font-weight: bold !important; background-color: #2c3e50 !important; /* Dark blue-grey for dark theme */ color: #e0e0e0 !important; } body.light-scheme .top-peer td { background-color: #e8f5e9 !important; /* Light green for light theme */ color: #1b5e20 !important; } .top-peer-1 td { color: #FF00FF !important; /* 1st: Neon Magenta */ font-size: 1.1em !important; } .top-peer-2 td { color: #00FFFF !important; /* 2nd: Neon Cyan */ font-size: 1.05em !important; } .top-peer-3 td { color: #39FF14 !important; /* 3rd: Neon Green */ } /* Neon cyan highlight for the logged-in username (exact text only) */ .tbd-neon-user { color: #00ffff !important; text-shadow: 0 0 2px #00ffff, 0 0 4px #00e5ff, 0 0 8px #00c3ff, 0 0 12px #00aaff; font-weight: 700; animation: tbd-neon-pulse 1.8s ease-in-out infinite; } @keyframes tbd-neon-pulse { 0%, 100% { text-shadow: 0 0 1px #00ffff, 0 0 3px #00e5ff, 0 0 6px #00c3ff, 0 0 9px #00aaff; } 50% { text-shadow: 0 0 3px #00ffff, 0 0 6px #00e5ff, 0 0 12px #00c3ff, 0 0 18px #00aaff; } } /* Glow for full row of logged-in user */ .tbd-row-glow td { color: #00ffff !important; text-shadow: 0 0 2px #00ffff, 0 0 4px #00e5ff, 0 0 8px #00c3ff, 0 0 12px #00aaff; animation: tbd-neon-pulse 1.8s ease-in-out infinite; } /* Add glow effect to the top three rows in addition to their color */ .top-peer-1 td, .top-peer-2 td, .top-peer-3 td { text-shadow: 0 0 1px currentColor, 0 0 3px currentColor, 0 0 6px currentColor; animation: tbd-top3-pulse 2.2s ease-in-out infinite; } @keyframes tbd-top3-pulse { 0%, 100% { text-shadow: 0 0 1px currentColor, 0 0 3px currentColor, 0 0 6px currentColor; } 50% { text-shadow: 0 0 2px currentColor, 0 0 6px currentColor, 0 0 10px currentColor; } } `; document.head.appendChild(style); function parseToBytes(uploadStr) { if (!uploadStr) return 0; const parts = uploadStr.trim().split(' '); if (parts.length < 2) return 0; let value = parseFloat(parts[0].replace(/,/g, '')); const unit = parts[1].toUpperCase(); switch (unit) { case 'TIB': return value * Math.pow(1024, 4); case 'GIB': return value * Math.pow(1024, 3); case 'MIB': return value * Math.pow(1024, 2); case 'KIB': return value * 1024; case 'B': return value; default: return 0; } } let refreshIntervalId = null; let cachedUsername = null; /** * Extract the logged-in username from the left profile card. * We read the visible card-title first, then fallback to the card-reveal. * Any inline icons/images are ignored; we use textContent trimmed. */ function getLoggedInUsername() { if (cachedUsername) return cachedUsername; try { // Primary: left-block card title -> .tbdrank span contains username (with optional <img>) const primary = document.querySelector('#left-block-container .card .card-content .card-title .tbdrank'); if (primary && primary.textContent) { const name = primary.textContent.trim(); if (name) { cachedUsername = name; return cachedUsername; } } // Fallback: card-reveal title (e.g., <span class="card-title">gaara</span>) const fallback = document.querySelector('#left-block-container .card .card-reveal .card-title'); if (fallback && fallback.textContent) { const name2 = fallback.textContent.trim(); if (name2) { cachedUsername = name2; return cachedUsername; } } } catch (e) { // ignore } return null; } /** * Remove prior neon wrappers to keep idempotency across refreshes. */ function removePreviousUsernameHighlights(scopeEl) { scopeEl.querySelectorAll('.tbd-neon-user').forEach(span => { const parent = span.parentNode; if (!parent) return; // Replace the wrapper span with a plain text node of its text const textNode = document.createTextNode(span.textContent); parent.replaceChild(textNode, span); parent.normalize(); // merge adjacent text nodes }); } /** * Wrap exact username text occurrences inside text nodes with a span. * Only operates within the provided root element. * Excludes text inside script/style. */ function wrapExactText(root, text, className) { if (!root || !text) return; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // Skip empty/whitespace if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; // Skip inside script/style const p = node.parentNode; if (!p) return NodeFilter.FILTER_REJECT; const tag = p.nodeName.toLowerCase(); if (tag === 'script' || tag === 'style') return NodeFilter.FILTER_REJECT; // Skip if already inside our highlight if (p.classList && p.classList.contains('tbd-neon-user')) return NodeFilter.FILTER_REJECT; // Consider return NodeFilter.FILTER_ACCEPT; } }); const pattern = new RegExp(`\\b${escapeRegExp(text)}\\b`, 'g'); // word-bounded exact match const nodesToProcess = []; while (walker.nextNode()) nodesToProcess.push(walker.currentNode); nodesToProcess.forEach(node => { const value = node.nodeValue; pattern.lastIndex = 0; let match; let lastIndex = 0; const fragments = []; while ((match = pattern.exec(value)) !== null) { if (match.index > lastIndex) { fragments.push(document.createTextNode(value.slice(lastIndex, match.index))); } const span = document.createElement('span'); span.className = className; span.textContent = match[0]; fragments.push(span); lastIndex = pattern.lastIndex; } if (fragments.length) { if (lastIndex < value.length) { fragments.push(document.createTextNode(value.slice(lastIndex))); } const parent = node.parentNode; fragments.forEach(f => parent.insertBefore(f, node)); parent.removeChild(node); parent.normalize(); } }); } function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } /** * Apply neon highlight for the logged-in username within the peers table. * Wrap exact username text and also glow the entire row that contains the username. */ function applyNeonHighlightInPeersTable() { const username = getLoggedInUsername(); if (!username) return; // Locate the peers table by the 'UL' header const ulHeader = Array.from(document.querySelectorAll('th')).find(th => th.textContent.trim() === 'UL'); if (!ulHeader) return; const peerTable = ulHeader.closest('table'); if (!peerTable) return; // Remove previous text highlights within this table removePreviousUsernameHighlights(peerTable); // Wrap exact username text in all cells for extra emphasis peerTable.querySelectorAll('td, th').forEach(cell => { wrapExactText(cell, username, 'tbd-neon-user'); }); // Add full-row glow for any row that contains the username peerTable.querySelectorAll('tbody tr').forEach(tr => { tr.classList.remove('tbd-row-glow'); const text = tr.textContent || ''; if (new RegExp(`\\b${escapeRegExp(username)}\\b`).test(text)) { tr.classList.add('tbd-row-glow'); } }); } function processPeerTable() { const ulHeader = Array.from(document.querySelectorAll('th')).find(th => th.textContent.trim() === 'UL'); if (!ulHeader) return; const headerRow = ulHeader.parentElement; const peerTable = headerRow.closest('table'); const tbody = peerTable.querySelector('tbody'); if (!headerRow || !peerTable || !tbody) return; // --- Cleanup phase: Restore table to a clean state before processing --- const oldRankHeader = headerRow.querySelector('.peer-rank-header'); if (oldRankHeader) oldRankHeader.remove(); const rows = Array.from(tbody.querySelectorAll('tr')); rows.forEach(row => { const rankCell = row.querySelector('.peer-rank-cell'); if (rankCell) rankCell.remove(); row.className = row.className.replace(/top-peer(-\d)?/g, '').trim(); row.classList.remove('tbd-row-glow'); }); // --- Processing phase: Now that the table is clean --- const originalHeaders = Array.from(headerRow.children); const ulColumnIndex = originalHeaders.findIndex(h => h.textContent.trim() === 'UL'); if (ulColumnIndex === -1) return; const rankHeader = document.createElement('th'); rankHeader.className = 'peer-rank-header'; rankHeader.textContent = 'Rank'; headerRow.insertBefore(rankHeader, headerRow.firstChild); const peers = []; rows.forEach(row => { const cells = row.cells; if (cells.length > ulColumnIndex) { const ulText = cells[ulColumnIndex].textContent; const ulBytes = parseToBytes(ulText); peers.push({ row, ul: ulBytes }); } else { peers.push({ row, ul: 0 }); } }); peers.sort((a, b) => b.ul - a.ul); peers.forEach((peer, index) => { if (index < 5) { peer.row.classList.add('top-peer'); if (index === 0) peer.row.classList.add('top-peer-1'); else if (index === 1) peer.row.classList.add('top-peer-2'); else if (index === 2) peer.row.classList.add('top-peer-3'); } const rankCell = document.createElement('td'); rankCell.className = 'peer-rank-cell'; rankCell.textContent = (index + 1).toString(); rankCell.style.textAlign = 'center'; peer.row.insertBefore(rankCell, peer.row.firstChild); tbody.appendChild(peer.row); }); // After processing and reordering, apply neon highlight for the logged-in username applyNeonHighlightInPeersTable(); } // Periodically detect the table and manage refreshes setInterval(() => { const ulHeader = Array.from(document.querySelectorAll('th')).find(th => th.textContent.trim() === 'UL'); if (ulHeader && !refreshIntervalId) { console.log("TBD Peer Rank: Peer table detected. Starting auto-refresh cycle."); // Ensure username is cached early if available getLoggedInUsername(); processPeerTable(); refreshIntervalId = setInterval(processPeerTable, 10000); } else if (!ulHeader && refreshIntervalId) { console.log("TBD Peer Rank: Peer table not found. Stopping auto-refresh cycle."); clearInterval(refreshIntervalId); refreshIntervalId = null; } else if (ulHeader && refreshIntervalId) { // Table still present; ensure highlight persists even if nothing else changed applyNeonHighlightInPeersTable(); } }, 500); })();