您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Colors @usernames you follow with stable huesr + profile-only add/remove UI.
// ==UserScript== // @name Color my Oomf // @namespace f_d_tools // @version 1.1 // @description Colors @usernames you follow with stable huesr + profile-only add/remove UI. // @author you // @match https://x.com/* // @match https://twitter.com/* // @grant none // ==/UserScript== (function () { 'use strict'; // ---- Config ---- const STORAGE_KEY = 'fd_following_handles_v1'; const HUE_BASE = 0; const HUE_SPREAD = 360; const SAT = 75; // % const LIT = 48; // % // ---- State ---- let following = new Set(loadFollowing()); const processedAttr = 'data-fd-colored'; // ---- Utils ---- function loadFollowing() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return []; const arr = JSON.parse(raw); return Array.isArray(arr) ? arr : []; } catch { return []; } } function saveFollowing() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(following))); } catch {} } // FNV-1a 32-bit (fast & simple) function hash32(str) { let h = 0x811c9dc5 >>> 0; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0; } return h >>> 0; } function colorForHandle(handle) { const h = (hash32(handle.toLowerCase()) % HUE_SPREAD + HUE_BASE) % 360; return `hsl(${h} ${SAT}% ${LIT}%)`; } function isAtHandle(text) { const t = text?.trim(); return !!t && t[0] === '@' && t.length > 1; } function extractHandleFromText(text) { return text.trim().replace(/^@/, '').split(/\s/)[0]; } function markIfFollowed(span) { if (!span || span.getAttribute(processedAttr)) return; const txt = span.textContent; if (!isAtHandle(txt)) return; const handle = extractHandleFromText(txt); if (!handle || !following.has(handle.toLowerCase())) return; colorSpan(span, handle); } function colorSpan(span, handle) { span.style.color = colorForHandle(handle); span.style.textDecorationColor = 'currentColor'; span.style.textDecorationThickness = 'from-font'; span.style.textDecorationLine = 'none'; span.setAttribute(processedAttr, '1'); } function uncolorAllInstances(handle) { // Remove color from any spans with this @handle const at = '@' + handle; document.querySelectorAll('span').forEach(s => { const t = s.textContent?.trim(); if (t === at) { s.style.color = ''; s.style.textDecorationColor = ''; s.style.textDecorationThickness = ''; s.style.textDecorationLine = ''; s.removeAttribute(processedAttr); } }); } // ---- Safe scanner on Following page ---- // Heuristic: only add handles from user cards that indicate a "Following" state, // which excludes "Follow" suggestions. function scanFollowingPageSafely() { let added = 0; // Candidates: user rows/cards const cards = document.querySelectorAll('[data-testid="UserCell"], [data-testid="cellInnerDiv"], article, div[role="listitem"]'); cards.forEach(card => { // Find a "Following" button/label inside this card const hasFollowingState = !!card.querySelector('div[role="button"][data-testid*="unfollow"], div[aria-label*="Following"], div[dir="auto"]:has(span:contains("Following"))') || textContains(card, /\bFollowing\b/); if (!hasFollowingState) return; // Extract @handle in this card const spans = card.querySelectorAll('span'); for (const s of spans) { const txt = s.textContent; if (!isAtHandle(txt)) continue; const h = extractHandleFromText(txt).toLowerCase(); // Sanity: link form /<handle> exists nearby (profile link) const profileLink = card.querySelector(`a[href^="/${cssEscape(h)}"]`); if (!profileLink) continue; if (!following.has(h)) { following.add(h); added++; } break; // one handle per card is enough } }); if (added > 0) saveFollowing(); return added; } // Fallback "visible scan" with extra guards; kept for the toolbar button function scanHandlesInFollowingPage() { // Prefer safe method return scanFollowingPageSafely(); } // ---- Profile-only Add/Remove UI ---- function onProfilePage() { // Heuristic: /handle or /handle/ with no further path segment // (exclude /home, /explore, etc.) const m = location.pathname.match(/^\/([A-Za-z0-9_]{1,15})(?:\/)?$/); return !!m && !['home', 'explore', 'notifications', 'messages', 'settings', 'compose', 'i'].includes(m[1]); } function currentProfileHandle() { // Try to read from header @handle const header = document.querySelector('[data-testid="UserName"]'); if (header) { const at = header.querySelector('span'); if (at && isAtHandle(at.textContent)) { return extractHandleFromText(at.textContent).toLowerCase(); } } // Fallback to path const m = location.pathname.match(/^\/([A-Za-z0-9_]{1,15})(?:\/)?$/); return m ? m[1].toLowerCase() : null; } let profileBarEl = null; function injectProfileBar() { removeProfileBar(); // avoid duplicates if (!onProfilePage()) return; const handle = currentProfileHandle(); if (!handle) return; const bar = document.createElement('div'); bar.id = 'fd-profile-pill'; bar.style.position = 'fixed'; bar.style.zIndex = 999999; bar.style.bottom = '16px'; bar.style.right = '16px'; bar.style.padding = '8px 10px'; bar.style.background = 'rgba(30,30,30,0.92)'; bar.style.color = '#fff'; bar.style.borderRadius = '9999px'; bar.style.boxShadow = '0 6px 20px rgba(0,0,0,0.35)'; bar.style.font = '12px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif'; bar.style.display = 'flex'; bar.style.gap = '8px'; bar.style.alignItems = 'center'; const label = document.createElement('span'); label.textContent = '@' + handle; label.style.opacity = '0.85'; function btn(text, onclick) { const b = document.createElement('button'); b.textContent = text; b.style.cursor = 'pointer'; b.style.border = '1px solid rgba(255,255,255,0.25)'; b.style.background = 'transparent'; b.style.color = '#fff'; b.style.padding = '4px 8px'; b.style.borderRadius = '9999px'; b.style.fontSize = '12px'; b.addEventListener('click', onclick); b.addEventListener('mouseenter', () => (b.style.background = 'rgba(255,255,255,0.08)')); b.addEventListener('mouseleave', () => (b.style.background = 'transparent')); return b; } const inCache = following.has(handle); const addBtn = btn('Add', () => { if (!following.has(handle)) { following.add(handle); saveFollowing(); // Recolor existing occurrences immediately document.querySelectorAll('span').forEach(s => { if (s.textContent?.trim() === '@' + handle) colorSpan(s, handle); }); injectProfileBar(); // refresh buttons } }); const removeBtn = btn('Remove', () => { if (following.delete(handle)) { saveFollowing(); uncolorAllInstances(handle); injectProfileBar(); // refresh buttons } }); bar.append(label, inCache ? removeBtn : addBtn); document.body.appendChild(bar); profileBarEl = bar; } function removeProfileBar() { if (profileBarEl && profileBarEl.parentNode) { profileBarEl.parentNode.removeChild(profileBarEl); } profileBarEl = null; } // ---- Following-page toolbar (unchanged UX) ---- function onFollowingPage() { return /\/following\/?$/.test(location.pathname); } let followToolbarEl = null; function injectFollowingToolbar() { if (followToolbarEl || !onFollowingPage()) return; const bar = document.createElement('div'); bar.id = 'fd-follow-toolbar'; bar.style.position = 'fixed'; bar.style.zIndex = 999999; bar.style.bottom = '16px'; bar.style.right = '16px'; bar.style.padding = '10px 12px'; bar.style.background = 'rgba(30,30,30,0.9)'; bar.style.color = '#fff'; bar.style.borderRadius = '12px'; bar.style.boxShadow = '0 6px 20px rgba(0,0,0,0.3)'; bar.style.font = '13px system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif'; bar.style.display = 'flex'; bar.style.gap = '8px'; bar.style.alignItems = 'center'; function btn(label, onclick, title='') { const b = document.createElement('button'); b.textContent = label; b.style.cursor = 'pointer'; b.style.border = '1px solid rgba(255,255,255,0.2)'; b.style.background = 'transparent'; b.style.color = '#fff'; b.style.padding = '6px 8px'; b.style.borderRadius = '8px'; b.style.fontSize = '12px'; if (title) b.title = title; b.addEventListener('click', onclick); b.addEventListener('mouseenter', () => (b.style.background = 'rgba(255,255,255,0.08)')); b.addEventListener('mouseleave', () => (b.style.background = 'transparent')); return b; } const status = document.createElement('span'); status.textContent = `Cached: ${following.size}`; status.style.opacity = '0.8'; const scanBtn = btn('Scan visible (safe)', () => { const added = scanHandlesInFollowingPage(); status.textContent = `Cached: ${following.size} (+${added})`; }, 'Adds @handles from cards that show a “Following” state, ignoring suggestions.'); const clearBtn = btn('Clear', () => { if (!confirm('Clear all cached handles?')) return; following.clear(); saveFollowing(); status.textContent = `Cached: ${following.size}`; }); const exportBtn = btn('Export', () => { const data = Array.from(following).join('\n'); navigator.clipboard.writeText(data).then(() => { status.textContent = `Copied ${following.size} handles`; setTimeout(() => (status.textContent = `Cached: ${following.size}`), 1500); }); }); bar.append(scanBtn, clearBtn, exportBtn, status); document.body.appendChild(bar); followToolbarEl = bar; } function removeFollowingToolbar() { if (followToolbarEl && followToolbarEl.parentNode) { followToolbarEl.parentNode.removeChild(followToolbarEl); } followToolbarEl = null; } // ---- Helpers ---- function textContains(root, regex) { const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null); let n; while ((n = walker.nextNode())) { if (regex.test(n.nodeValue)) return true; } return false; } // Escape for CSS attribute selector usage function cssEscape(str) { return CSS?.escape ? CSS.escape(str) : str.replace(/[^a-zA-Z0-9_\-]/g, s => '\\' + s); } // ---- DOM observation ---- const observer = new MutationObserver(mutations => { for (const m of mutations) { for (const node of m.addedNodes) { if (!(node instanceof HTMLElement)) continue; if (node.tagName === 'SPAN') { markIfFollowed(node); } const spans = node.querySelectorAll?.('span'); if (spans?.length) { for (const s of spans) markIfFollowed(s); } } } }); function primeExisting() { document.querySelectorAll('span').forEach(markIfFollowed); } function routeChangeHandlers() { // Called on SPA route changes primeExisting(); removeFollowingToolbar(); removeProfileBar(); if (onFollowingPage()) injectFollowingToolbar(); if (onProfilePage()) injectProfileBar(); } // ---- Boot ---- primeExisting(); observer.observe(document.documentElement || document.body, { childList: true, subtree: true }); // SPA navigation watcher let lastPath = location.pathname; setInterval(() => { if (location.pathname !== lastPath) { lastPath = location.pathname; routeChangeHandlers(); } }, 800); // First-time page kind setup if (onFollowingPage()) injectFollowingToolbar(); if (onProfilePage()) injectProfileBar(); })();