您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Append GGn class emoji to usernames with a customizable UI
// ==UserScript== // @name GGn TheLounge User Data Enhancement // @version 3.1.2 // @author SleepingGiant // @description Append GGn class emoji to usernames with a customizable UI // @namespace https://gf.qytechs.cn/users/1395131 // @include *:9000/* // @grant GM.xmlHttpRequest // @grant GM.getValue // @grant GM.setValue // @grant GM.listValues // @connect gazellegames.net // ==/UserScript== (function () { 'use strict'; if (typeof window.requestIdleCallback !== 'function') { window.requestIdleCallback = function (cb) { return setTimeout(cb, 0); }; } console.debug("GGn TheLounge User Data Enhancement starting..."); const API_URL = 'https://gazellegames.net/api.php'; const RATE_LIMIT_MS = 3000; const CACHE_DURATION_MS = 36 * 60 * 60 * 1000; // 1.5 days. Makes loading smoother and people's user classes don't change much. let lastApiCallTime = Date.now(); const uncachedUsers = new Set(); const defaultClassToEmoji = { "Amateur": "👶", "Gamer": "🎲", "Pro Gamer": "🕹️", "Elite Gamer": "🎮", "Legendary Gamer": "🌟", "Master Gamer": "⚡", "Gaming God": "🏆", "Staff Trainee": "🛠️", "Moderator": "🧹", "Senior Moderator": "🛡️", "Team Leader": "🥇", "Junior Developer": "💻", "Senior Developer": "💻", "SysOp": "⚙️", "Administrator": "👑", "Uploader": "📦", "VIP": "💎", "VIP+": "💎", "Legend": "💎", "Bot": "🤖", "undefined": "❓", // Below here are not actual user classes but I am too lazy to rewrite it all for this. "prepend_warn": "⚠️", "prepend_disabled": "🚫", "prepend_friend": "❤️", "low_gold": "⬇️💰", "negative_gold": "❗💰", }; let classToEmoji = {}; async function loadClassMap() { console.debug("Loading class emoji map..."); const saved = await GM.getValue("customClassMap"); const savedMap = saved ? JSON.parse(saved) : {}; // Build classToEmoji using only keys from defaultClassToEmoji. Allows for default on new keys and removing old keys naturally. classToEmoji = {}; for (const key of Object.keys(defaultClassToEmoji)) { classToEmoji[key] = key in savedMap ? savedMap[key] : defaultClassToEmoji[key]; } console.debug("Loaded classToEmoji:", classToEmoji); } const defaultEmojiVisibility = { class: true, warn: true, disabled: true, friend: true, lowGold: false, lowGoldLowClass: true, negativeGold: true, }; let emojiVisibility = {}; async function loadEmojiVisibility() { console.debug("Loading emoji visibility settings..."); const saved = await GM.getValue("emojiVisibilitySettings"); const savedMap = saved ? JSON.parse(saved) : {}; emojiVisibility = {}; for (const key of Object.keys(defaultEmojiVisibility)) { emojiVisibility[key] = key in savedMap ? savedMap[key] : defaultEmojiVisibility[key]; } console.debug("Loaded emojiVisibility:", emojiVisibility); } async function saveClassMap() { console.debug("Saving class emoji map:", classToEmoji); await GM.setValue("customClassMap", JSON.stringify(classToEmoji)); } async function getApiKey() { console.debug("Getting API key..."); let apiKey = await GM.getValue("apiKey"); if (!apiKey) { apiKey = prompt("Enter your GazelleGames API key. Required permissions: \"User\":")?.trim(); if (apiKey) await GM.setValue("apiKey", apiKey); } return apiKey; } async function getCached(username) { const key = `userCache_${username}`; const raw = await GM.getValue(key); if (!raw) return null; const { timestamp, data } = JSON.parse(raw); const expired = (Date.now() - timestamp) > CACHE_DURATION_MS; if (!expired && data && Object.keys(data).length > 0) return data; console.debug(`Cache miss or expired for ${username}`); return null; } async function setCached(username, data) { console.debug(`Caching data for user: ${username}`, data); const key = `userCache_${username}`; await GM.setValue(key, JSON.stringify({ timestamp: Date.now(), data })); } async function fetchUserInfo(username) { console.debug(`Fetching user info for: ${username}`); const now = Date.now(); if ((now - lastApiCallTime) < RATE_LIMIT_MS) { console.debug("Rate limit hit, skipping request"); return null; } lastApiCallTime = now; const apiKey = await getApiKey(); if (!apiKey) { console.debug("No API key available"); return null; } const url = `${API_URL}?key=${encodeURIComponent(apiKey)}&request=user&name=${encodeURIComponent(username)}`; return new Promise(resolve => { GM.xmlHttpRequest({ method: 'GET', url, onload: res => { try { const data = JSON.parse(res.responseText); console.debug("Response data:") console.debug(data); if (res.status !== 200 || data?.status === "failure") { console.debug("API error response:", data); resolve(data?.error === "no such user" ? { invalidUser: true } : null); } else resolve(data); } catch { resolve(null); } }, onerror: (e) => { console.debug("Request error:", e); resolve(null); } }); }); } async function annotateUser(userEl) { const username = userEl.getAttribute('data-name'); if (!username || userEl.dataset.emojiAppended === "true") return; let data = await getCached(username); if (!data) { console.debug(`User ${username} not cached, adding to queue`); uncachedUsers.add(username); return; } const personal = data?.response?.personal; const userClass = personal?.class; const isWarned = personal?.warned; const isDisabled = !personal?.enabled; const isFriend = data?.response?.isFriend; console.debug(username, isFriend); const emoji = classToEmoji[userClass]; const warnEmoji = classToEmoji["prepend_warn"] ?? ""; const disabledEmoji = classToEmoji["prepend_disabled"] ?? ""; const friendEmoji = classToEmoji["prepend_friend"] ?? ""; const lowGoldEmoji = classToEmoji["low_gold"] ?? ""; const negativeGoldEmoji = classToEmoji["negative_gold"] ?? ""; const lowClass = ["Amateur", "Gamer", "Pro Gamer"].includes(userClass); const goldValue = data?.response?.stats?.gold; let goldEmoji = ''; if (goldValue !== undefined && goldValue !== null) { const isNegative = emojiVisibility.negativeGold && goldValue < 0; const isLowGold = (emojiVisibility.lowGoldLowClass && lowClass && goldValue < 10000) || (emojiVisibility.lowGold && goldValue < 10000); if (isNegative) { goldEmoji += negativeGoldEmoji; } else if (isLowGold) { goldEmoji += lowGoldEmoji; } } const prependEmoji = (emojiVisibility.warn && isWarned ? warnEmoji : '') + (emojiVisibility.disabled && isDisabled ? disabledEmoji : '') + (emojiVisibility.friend && isFriend ? friendEmoji : '') + goldEmoji; userEl.dataset.emojiAppended = "true"; // Clear old emoji spans if re-rendering userEl.querySelectorAll('.inline-emoji').forEach(e => e.remove()); // Prepend span if (prependEmoji.trim()) { const pre = document.createElement('span'); pre.className = 'inline-emoji'; pre.textContent = prependEmoji; pre.style.marginRight = '4px'; pre.style.pointerEvents = 'none'; pre.style.userSelect = 'none'; userEl.insertBefore(pre, userEl.firstChild); } if (emojiVisibility.class && emoji) { const post = document.createElement('span'); post.className = 'inline-emoji'; post.textContent = emoji; post.style.marginLeft = '4px'; post.style.pointerEvents = 'none'; post.style.userSelect = 'none'; userEl.appendChild(post); } } async function scanRecentMessages() { console.debug("Scanning recent messages..."); const seen = new WeakSet(); const messages = Array.from(document.querySelectorAll('.msg[data-type="message"] .user')).reverse(); for (const userEl of messages) { if (!seen.has(userEl) && userEl.dataset.emojiAppended !== "true") { seen.add(userEl); requestIdleCallback(() => annotateUser(userEl)); } } } function init() { console.debug("Initializing message observer..."); const msgContainer = document.querySelector('.messages'); if (!msgContainer) { console.debug("No message container found."); return; } const style = document.createElement('style'); document.head.insertAdjacentHTML('beforeend', ` <style> .inline-emoji { font-size: inherit; vertical-align: middle; } </style> `); document.head.appendChild(style); new MutationObserver(scanRecentMessages).observe(msgContainer, { childList: true, subtree: true }); injectCSS(); scanRecentMessages(); } function injectCSS() { const css = ` #chat .from { width: 190px !important; } .inline-emoji { font-size: inherit; vertical-align: middle; } `; const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); } (async () => { console.debug("Running main async block"); await loadClassMap(); await loadEmojiVisibility(); const appCheck = setInterval(() => { const app = document.getElementById('app'); if (app) { clearInterval(appCheck); console.debug("App container found, initializing UI"); addScriptSettingsButton(); init(); openUserContextMenu(); } }, 100); // Background fetching loop. If you see 50 uncached users on first load for example, queue them up and iterate over, makes initial load easier. setInterval(async () => { const [username] = uncachedUsers; if (!username) return; const data = await fetchUserInfo(username); if (data) { await setCached(username, data); uncachedUsers.delete(username); // only now! scanRecentMessages(); // will paint the emoji } }, RATE_LIMIT_MS); setInterval(async () => { scanRecentMessages(); }, 60000) // Every 60 seconds do a rescan. Force pulse check (attempting to debug emojis stopping rendering. This does not need to stay) })(); // ------------ Custom Emoji Edit button below (nothing to do with actual user polling) ---------- function createSettingsUI() { let currentRenderFn = null; console.debug("Creating routed settings UI"); const modal = document.createElement('div'); modal.style = ` position: fixed; top: 50%; left: 50%; background: #fff; color: #000; border: 2px solid #ccc; border-radius: 10px; padding: 0; z-index: 9999; width: 400px; box-shadow: 0 0 10px rgba(0,0,0,0.3); overflow: hidden; font-family: sans-serif; transform: translate(-50%, -50%); `; modal.setAttribute("id", "enhancementSettingsModal"); const header = document.createElement('div'); header.style = ` background-color: #eee; padding: 10px; font-weight: bold; position: relative; border-bottom: 1px solid #ccc; display: flex; align-items: center; justify-content: space-between; `; const titleContainer = document.createElement('div'); titleContainer.style = 'display: flex; align-items: center; gap: 10px;'; const backBtn = document.createElement('span'); backBtn.innerHTML = '←'; backBtn.style = `cursor: pointer; font-size: 18px; display: none;`; const title = document.createElement('span'); title.textContent = 'Enhancement Settings'; titleContainer.appendChild(backBtn); titleContainer.appendChild(title); const closeBtn = document.createElement('span'); closeBtn.innerHTML = '×'; closeBtn.style = `color: red; font-size: 18px; font-weight: bold; cursor: pointer;`; closeBtn.onclick = () => modal.remove(); header.appendChild(titleContainer); header.appendChild(closeBtn); const content = document.createElement('div'); content.style.padding = '10px'; modal.appendChild(header); modal.appendChild(content); document.body.appendChild(modal); const routeStack = []; function setPage(titleText, renderFn) { currentRenderFn = renderFn; title.textContent = titleText; content.innerHTML = ''; renderFn(content); backBtn.style.display = routeStack.length > 0 ? 'inline' : 'none'; } backBtn.onclick = () => { const prev = routeStack.pop(); if (prev) setPage(prev.title, prev.renderFn); }; function setLandingPage() { routeStack.length = 0; setPage('Enhancement Settings', (container) => { const options = [ ['Emojis on Name', () => setSubPage('Emojis on Name', renderEmojiEditor)], ['Profile Visibility', () => setSubPage('Profile Visibility', renderProfileVisibilitySettingsPage)], ['Cached Warnings', () => setSubPage('Users With Warnings', renderWarningList)], ['Emoji Visibility', () => setSubPage('Emoji Visibility', renderEmojiVisibilityPage)], ]; options.forEach(([label, onClick]) => { const btn = document.createElement('button'); btn.textContent = label; btn.style = ` display: block; width: 100%; margin-bottom: 10px; padding: 10px; font-size: 16px; border-radius: 6px; cursor: pointer; border: 1px solid #ccc; background: #f9f9f9; `; btn.onclick = onClick; container.appendChild(btn); }); }); } function setSubPage(titleText, renderFn) { routeStack.push({ title: title.textContent, renderFn: currentRenderFn }); setPage(titleText, renderFn); } makeElementDraggable(modal, header); setLandingPage(); } function renderEmojiVisibilityPage(container) { const form = document.createElement('form'); form.style.maxHeight = '300px'; form.style.overflowY = 'auto'; const checkboxes = {}; const fields = [ ['class', 'Show Class Emojis'], ['warn', 'Show Warn Emoji'], ['disabled', 'Show Disabled Emoji'], ['friend', 'Show Friend Emoji'], ['lowGold', 'Show Low Gold Emoji (All)'], ['lowGoldLowClass', 'Show Low Gold Emoji (Power User and Below)'], ['negativeGold', 'Show Negative Gold Emoji'], ]; fields.forEach(([key, label]) => { const row = document.createElement('label'); row.style.display = 'block'; row.style.marginBottom = '6px'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = emojiVisibility[key]; checkboxes[key] = checkbox; row.appendChild(checkbox); row.append(` ${label}`); form.appendChild(row); }); const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.style = `margin-top: 10px; margin-right: 10px; padding: 8px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 5px;`; saveBtn.onclick = async (e) => { e.preventDefault(); for (const key in checkboxes) { emojiVisibility[key] = checkboxes[key].checked; } await GM.setValue("emojiVisibilitySettings", JSON.stringify(emojiVisibility)); document.querySelector('#enhancementSettingsModal span').click(); }; const resetBtn = document.createElement('button'); resetBtn.textContent = 'Reset'; resetBtn.style = `margin-top: 10px; padding: 8px 12px; background-color: #f44336; color: white; border: none; border-radius: 5px;`; resetBtn.onclick = async (e) => { e.preventDefault(); for (const key in defaultEmojiVisibility) { emojiVisibility[key] = defaultEmojiVisibility[key]; checkboxes[key].checked = defaultEmojiVisibility[key]; } await GM.setValue("emojiVisibilitySettings", JSON.stringify(emojiVisibility)); }; container.appendChild(form); container.appendChild(saveBtn); container.appendChild(resetBtn); } function makeElementDraggable(el, handle) { let offsetX = 0, offsetY = 0, isDragging = false; handle.style.cursor = 'move'; handle.addEventListener('mousedown', function (e) { isDragging = true; // Remove transform if still present and switch to absolute positioning. Without this on first click it'll "jump" around if (el.style.transform.includes('translate')) { const rect = el.getBoundingClientRect(); el.style.left = `${rect.left}px`; el.style.top = `${rect.top}px`; el.style.transform = 'none'; } offsetX = e.clientX - el.offsetLeft; offsetY = e.clientY - el.offsetTop; document.addEventListener('mousemove', moveHandler); document.addEventListener('mouseup', upHandler); e.preventDefault(); }); function moveHandler(e) { if (!isDragging) return; el.style.left = `${e.clientX - offsetX}px`; el.style.top = `${e.clientY - offsetY}px`; } function upHandler() { isDragging = false; document.removeEventListener('mousemove', moveHandler); document.removeEventListener('mouseup', upHandler); } } function renderEmojiEditor(container) { const form = document.createElement('form'); form.style.maxHeight = '300px'; form.style.overflowY = 'auto'; Object.entries(classToEmoji).forEach(([cls, emoji]) => { const row = document.createElement('div'); row.innerHTML = ` <label style="display:flex;justify-content:space-between;margin-bottom:6px"> <span>${cls}</span> <input type="text" value="${emoji}" data-class="${cls}" style="width: 50px; text-align:center" /> </label> `; form.appendChild(row); }); const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.style = `margin-top: 10px; margin-right: 10px; padding: 8px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 5px;`; saveBtn.onclick = async (e) => { e.preventDefault(); const inputs = form.querySelectorAll('input'); classToEmoji = {}; inputs.forEach(input => { const cls = input.getAttribute('data-class'); classToEmoji[cls] = input.value; }); await saveClassMap(); document.querySelector('#enhancementSettingsModal span').click(); }; const resetBtn = document.createElement('button'); resetBtn.textContent = 'Reset'; resetBtn.style = `margin-top: 10px; padding: 8px 12px; background-color: #f44336; color: white; border: none; border-radius: 5px;`; resetBtn.onclick = async (e) => { e.preventDefault(); if (!confirm('Reset emoji mappings to default?')) return; classToEmoji = { ...defaultClassToEmoji }; await saveClassMap(); renderEmojiEditor(container); }; container.appendChild(form); container.appendChild(saveBtn); container.appendChild(resetBtn); } function renderProfileVisibilitySettingsPage(container) { const form = document.createElement('form'); form.style.maxHeight = '300px'; form.style.overflowY = 'auto'; const selected = new Set(getSelectedFields()); const checkboxes = {}; const fieldOptions = [ // top-level ['id', 'User ID'], ['username', 'Username'], ['avatar', 'Avatar'], ['avatarType', 'Avatar Type'], ['isFriend', 'Friend'], // stats ['joinedDate', 'Joined'], ['lastAccess', 'Last Seen'], ['onIRC', 'IRC'], ['uploaded', 'Uploaded'], ['downloaded', 'Downloaded'], ['fullDownloaded', 'Full Downloaded'], ['purchasedDownload', 'Purchased Download'], ['ratio', 'Ratio'], ['requiredRatio', 'Required Ratio'], ['shareScore', 'Share Score'], ['gold', 'Gold'], // personal ['class', 'Class'], ['facilitator', 'Facilitator'], ['hnrs', 'HNRS'], ['donor', 'Donor'], ['warned', 'Warned'], ['enabled', 'Enabled'], ['publicKey', 'Public Key'], ['parked', 'Parked'], ['paranoiaText', 'Paranoia'], // community ['clan', 'Clan'], ['profileViews', 'Profile Views'], ['hourlyGold', 'Hourly Gold'], ['posts', 'Forum Posts'], ['actualPosts', 'Raw Forum Posts'], ['threads', 'Threads'], ['forumLikes', 'Likes'], ['forumDislikes', 'Dislikes'], ['ircLines', 'IRC Lines'], ['ircActualLines', 'Raw IRC Lines'], ['torrentComments', 'Torrent Comments'], ['collections', 'Collections'], ['requestsFilled', 'Requests Filled'], ['bountyEarnedUpload', 'Bounty Earned (Upload)'], ['bountyEarnedGold', 'Bounty Earned (Gold)'], ['requestsVoted', 'Requests Voted'], ['bountySpentUpload', 'Bounty Spent (Upload)'], ['bountySpentGold', 'Bounty Spent (Gold)'], ['reviews', 'Reviews'], ['seeding', 'Seeding'], ['leeching', 'Leeching'], ['snatched', 'Snatched'], ['uniqueSnatched', 'Unique Snatched'], ['seedSize', 'Seed Size'], ['invited', 'Invited users'], // buffs ['buffUpload', 'Buff: Upload'], ['buffDownload', 'Buff: Download'], ['buffForumPosts', 'Buff: Forum Posts'], ['buffIRCLines', 'Buff: IRC Lines'], ['buffIRCBonus', 'Buff: IRC Bonus'], ['buffCommunityXP', 'Buff: Community XP'], ['buffTorrentsXP', 'Buff: Torrents XP'], ['buffCommunityGold', 'Buff: Community Gold'], ['buffTorrentsGold', 'Buff: Torrents Gold'], ['buffItemCost', 'Buff: Item Cost'], ['buffBountyFrom', 'Buff: Bounty From'], ['buffBountyOn', 'Buff: Bounty On'], ['buffChance', 'Buff: Chance'] ]; fieldOptions.forEach(([key, label]) => { const row = document.createElement('label'); row.style.display = 'block'; row.style.marginBottom = '6px'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = selected.has(key); checkboxes[key] = checkbox; checkbox.addEventListener('change', () => { if (checkbox.checked) selected.add(key); else selected.delete(key); setSelectedFields(Array.from(selected)); }); row.appendChild(checkbox); row.append(' ' + label); form.appendChild(row); }); const saveBtn = document.createElement('button'); saveBtn.textContent = 'Save'; saveBtn.style = `margin-top: 10px; margin-right: 10px; padding: 8px 12px; background-color: #4CAF50; color: white; border: none; border-radius: 5px;`; saveBtn.onclick = (e) => { e.preventDefault(); // just close via back button document.querySelector('#enhancementSettingsModal span').click(); }; const resetBtn = document.createElement('button'); resetBtn.textContent = 'Reset'; resetBtn.style = `margin-top: 10px; padding: 8px 12px; background-color: #f44336; color: white; border: none; border-radius: 5px;`; resetBtn.onclick = (e) => { e.preventDefault(); const defaults = new Set(JSON.parse(defaultProfileVisibilitySelectedFiels)); // Uncheck everything, then re-check only defaults Object.entries(checkboxes).forEach(([key, chk]) => { const shouldBeChecked = defaults.has(key); chk.checked = shouldBeChecked; if (shouldBeChecked) selected.add(key); else selected.delete(key); }); setSelectedFields(Array.from(selected)); }; container.appendChild(form); container.appendChild(saveBtn); container.appendChild(resetBtn); } async function renderWarningList(container) { const form = document.createElement('form'); const keys = await GM.listValues(); const warningUsers = []; const now = Date.now(); for (const key of keys) { if (key.startsWith('userCache_')) { const raw = await GM.getValue(key); if (!raw) continue; let parsed; try { parsed = JSON.parse(raw); } catch { continue; // skip malformed cache } const { timestamp, data } = parsed; const expired = (now - timestamp) > CACHE_DURATION_MS; if (expired || !data?.response?.personal?.warned) continue; const username = key.replace('userCache_', ''); warningUsers.push(username); } } if (warningUsers.length === 0) { container.innerHTML = `<p>No users with valid, cached warnings.</p>`; return; } const ul = document.createElement('ul'); warningUsers.forEach(user => { const li = document.createElement('li'); li.textContent = user; ul.appendChild(li); }); ul.style.maxHeight = '300px'; ul.style.overflowY = 'auto'; ul.style.paddingLeft = '20px'; container.appendChild(ul); } function addScriptSettingsButton() { console.debug("Attempting to add settings button"); const footerCheck = setInterval(() => { const footer = document.getElementById('footer'); if (footer) { clearInterval(footerCheck); console.debug("Footer found, adding button"); const btn = document.createElement('button'); btn.textContent = 'Edit Enhancer'; btn.style = 'margin-left: 10px; padding: 4px 8px; font-size: 12px;'; btn.onclick = createSettingsUI; footer.appendChild(btn); } }, 1000); } // ------------ Custom User Selection Data Enhancement below (nothing to do with actual user polling) ---------- function openUserContextMenu() { const observer = new MutationObserver(async (mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if ( node.nodeType === 1 && node.id === 'context-menu-container' ) { const menu = node.querySelector('#context-menu'); const userItem = menu?.querySelector('.context-menu-user'); const username = userItem?.textContent?.trim(); if (!username) return; const data = await getCached(username); if (!data?.response) return; const { isFriend, stats, id } = data.response; const profileURL = `https://gazellegames.net/user.php?id=${id}`; // Wrap existing items in a new container const existingItems = Array.from(menu.children).filter(child => child.tagName === 'LI' || child.classList.contains('context-menu-divider')); const leftDiv = document.createElement('div'); leftDiv.style.flex = '1'; existingItems.forEach(item => leftDiv.appendChild(item)); const bg = window.getComputedStyle(menu).backgroundColor; const rgb = bg.match(/\d+/g).map(Number); const brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 255000; // Normalize 0-1. const textColor = brightness < 0.4 ? 'white' : 'black'; // Give slight favor to black (up to 60%) const rows = []; function addRow(label, value) { if (value !== undefined && value !== null && !/^\s*$/.test(value)) // regex checks for only whitespace rows.push(`<div><strong>${label}:</strong> ${value}</div>`); } const s = data.response.stats || {}; const p = data.response.personal || {}; const c = data.response.community || {}; const b = data.response.buffs || {}; const top = data.response || {}; const selectedFields = getSelectedFields(); if (selectedFields.includes('avatar')) { const avatarItem = document.createElement('li'); avatarItem.className = 'context-menu-item'; avatarItem.role = 'menuitem'; avatarItem.innerHTML = `<img src="${top.avatar}" style="max-height:80px; vertical-align: middle;">`; leftDiv.insertBefore(avatarItem, leftDiv.children[1]); // Insert right after the username } if (selectedFields.includes('id')) addRow('User ID', top.id); if (selectedFields.includes('username')) addRow('Username', top.username); if (selectedFields.includes('avatarType')) addRow('Avatar Type', top.avatarType); if (selectedFields.includes('profile')) { rows.push(`<div><a href="${profileURL}" target="_blank" style="color: #4ea1d3;">Profile</a></div>`); } if (selectedFields.includes('isFriend')) addRow('Friend', isFriend ? '✅' : '❌'); if (selectedFields.includes('joinedDate')) addRow('Joined', formatDate(s.joinedDate)); if (selectedFields.includes('lastAccess')) addRow('Last Seen', formatDate(s.lastAccess)); if (selectedFields.includes('onIRC')) addRow('IRC', s.onIRC ? '✅' : '❌'); if (selectedFields.includes('gold')) { let gold = s.gold; if (typeof gold === 'string' && gold.includes('-')) { gold = `<span style="color:red; font-weight:bold;">${gold}</span>`; } addRow('Gold', gold); } if (selectedFields.includes('uploaded')) addRow('Uploaded', formatBytes(s.uploaded)); if (selectedFields.includes('downloaded')) addRow('Downloaded', formatBytes(s.downloaded)); if (selectedFields.includes('fullDownloaded')) addRow('Full Downloaded', formatBytes(s.fullDownloaded)); if (selectedFields.includes('purchasedDownload')) addRow('Purchased Download', formatBytes(s.purchasedDownload)); if (selectedFields.includes('ratio')) addRow('Ratio', s.ratio); if (selectedFields.includes('requiredRatio')) addRow('Required Ratio', s.requiredRatio); if (selectedFields.includes('shareScore')) addRow('Share Score', s.shareScore); if (selectedFields.includes('class')) addRow('Class', p.class); if (selectedFields.includes('facilitator')) addRow('Facilitator', p.facilitator ? '✅' : '❌'); if (selectedFields.includes('hnrs')) addRow('HNRS', p.hnrs); if (selectedFields.includes('donor')) addRow('Donor', p.donor ? '✅' : '❌'); if (selectedFields.includes('warned')) addRow('Warned', p.warned ? '⚠️' : 'No'); if (selectedFields.includes('enabled')) addRow('Enabled', p.enabled ? '✅' : '❌'); if (selectedFields.includes('publicKey')) addRow('Public Key', p.publicKey); if (selectedFields.includes('parked')) addRow('Parked', p.parked ? '✅' : '❌'); if (selectedFields.includes('paranoiaText')) addRow('Paranoia', p.paranoiaText); if (selectedFields.includes('clan')) addRow('Clan', c.clan); if (selectedFields.includes('profileViews')) addRow('Profile Views', c.profileViews); if (selectedFields.includes('hourlyGold')) addRow('Hourly Gold', c.hourlyGold); if (selectedFields.includes('posts')) addRow('Forum Posts', c.posts); if (selectedFields.includes('actualPosts')) addRow('Raw Forum Posts', c.actualPosts); if (selectedFields.includes('threads')) addRow('Threads', c.threads); if (selectedFields.includes('forumLikes')) addRow('Likes', c.forumLikes); if (selectedFields.includes('forumDislikes')) addRow('Dislikes', c.forumDislikes); if (selectedFields.includes('ircLines')) addRow('IRC Lines', c.ircLines); if (selectedFields.includes('ircActualLines')) addRow('Raw IRC Lines', c.ircActualLines); if (selectedFields.includes('torrentComments')) addRow('Torrent Comments', c.torrentComments); if (selectedFields.includes('collections')) addRow('Collections', c.collections); if (selectedFields.includes('requestsFilled')) addRow('Requests Filled', c.requestsFilled); if (selectedFields.includes('bountyEarnedUpload')) addRow('Bounty Earned (Upload)', c.bountyEarnedUpload); if (selectedFields.includes('bountyEarnedGold')) addRow('Bounty Earned (Gold)', c.bountyEarnedGold); if (selectedFields.includes('requestsVoted')) addRow('Requests Voted', c.requestsVoted); if (selectedFields.includes('bountySpentUpload')) addRow('Bounty Spent (Upload)', c.bountySpentUpload); if (selectedFields.includes('bountySpentGold')) addRow('Bounty Spent (Gold)', c.bountySpentGold); if (selectedFields.includes('reviews')) addRow('Reviews', c.reviews); if (selectedFields.includes('seeding')) addRow('Seeding', c.seeding); if (selectedFields.includes('leeching')) addRow('Leeching', c.leeching); if (selectedFields.includes('snatched')) addRow('Snatched', c.snatched); if (selectedFields.includes('uniqueSnatched')) addRow('Unique Snatched', c.uniqueSnatched); if (selectedFields.includes('seedSize')) addRow('Seed Size', formatBytes(c.seedSize)); if (selectedFields.includes('invited')) addRow('Invited Users', c.invited); if (selectedFields.includes('buffUpload')) addRow('Buff: Upload', b.Upload); if (selectedFields.includes('buffDownload')) addRow('Buff: Download', b.Download); if (selectedFields.includes('buffForumPosts')) addRow('Buff: Forum Posts', b.ForumPosts); if (selectedFields.includes('buffIRCLines')) addRow('Buff: IRC Lines', b.IRCLines); if (selectedFields.includes('buffIRCBonus')) addRow('IRC Bonus', b.IRCBonus); if (selectedFields.includes('buffCommunityXP')) addRow('Community XP', b.CommunityXP); if (selectedFields.includes('buffTorrentsXP')) addRow('Torrents XP', b.TorrentsXP); if (selectedFields.includes('buffCommunityGold')) addRow('Community Gold', b.CommunityGold); if (selectedFields.includes('buffTorrentsGold')) addRow('Torrents Gold', b.TorrentsGold); if (selectedFields.includes('buffItemCost')) addRow('Shop Discount', b.ItemCost); if (selectedFields.includes('buffBountyFrom')) addRow('Bounty From', b.BountyFrom); if (selectedFields.includes('buffBountyOn')) addRow('Bounty On', b.BountyOn); if (selectedFields.includes('buffChance')) addRow('Chance', b.Chance); const MAX_ROWS_PER_COLUMN = 10; const rightWrapper = document.createElement('div'); rightWrapper.style.display = 'flex'; rightWrapper.style.gap = '10px'; rightWrapper.style.width = 'fit-content'; rightWrapper.style.flex = '0 0 auto'; rightWrapper.style.userSelect = 'text'; rightWrapper.style.pointerEvents = 'auto'; rightWrapper.style.flexWrap = 'nowrap'; // Prevent flex items from wrapping to new lines rightWrapper.addEventListener('contextmenu', e => e.stopPropagation()); // Group rows into columns for (let i = 0; i < rows.length; i += MAX_ROWS_PER_COLUMN) { const col = document.createElement('div'); col.style.flex = '1'; col.style.fontSize = '0.85em'; col.style.whiteSpace = 'nowrap'; col.style.color = textColor; const columnRows = rows.slice(i, i + MAX_ROWS_PER_COLUMN).join(''); col.innerHTML = columnRows; rightWrapper.appendChild(col); } // Clear menu and insert flex container menu.innerHTML = ''; menu.style.display = 'flex'; menu.style.flexDirection = 'row'; menu.style.minWidth = '250px'; menu.appendChild(leftDiv); menu.appendChild(rightWrapper); } } } }); observer.observe(document.body, { childList: true, subtree: true }); } function formatBytes(bytes) { if (bytes === null) { return null; } const MB = 1024 ** 2; const GB = 1024 ** 3; const TB = 1024 ** 4; const PB = 1024 ** 5; if (bytes >= PB) { return (bytes / PB).toFixed(3) + ' PB'; } else if (bytes >= TB) { return (bytes / TB).toFixed(3) + ' TB'; } else if (bytes >= GB) { return (bytes / GB).toFixed(3) + ' GB'; } else { return (bytes / MB).toFixed(3) + ' MB'; } } function formatDate(dateStr) { return dateStr.split(' ')[0]; // YYYY-MM-DD } const defaultProfileVisibilitySelectedFiels = `[ "avatar", "isFriend", "gold", "profile", "joinedDate", "uploaded", "downloaded", "ratio", "shareScore", "class", "donor", "warned", "torrentsUploaded", "paranoia", "hourlyGold", "posts", "threads", "forumDislikes", "forumLikes", "ircLines", "seedSize" ]`; function getSelectedFields() { return JSON.parse(localStorage.getItem('selectedUserFields') || defaultProfileVisibilitySelectedFiels); } function setSelectedFields(fields) { try { localStorage.setItem('selectedUserFields', JSON.stringify(fields)); } catch (e) { console.error('Failed to save selected fields:', e); } } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址