您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds colors and expand/collapse functionality to Bluesky threads
// ==UserScript== // @name Bluesky Threading Improvements // @namespace zetaphor.com // @description Adds colors and expand/collapse functionality to Bluesky threads // @version 0.4 // @license MIT // @match https://bsky.app/* // @grant GM_addStyle // ==/UserScript== (function() { 'use strict'; // Add our styles GM_addStyle(` /* Thread depth colors */ div[style*="border-left-width: 2px"] { border-left-width: 2px !important; border-left-style: solid !important; } /* Color definitions */ div[style*="border-left-width: 2px"]:nth-child(1) { border-color: #2962ff !important; } /* Blue */ div[style*="border-left-width: 2px"]:nth-child(2) { border-color: #8e24aa !important; } /* Purple */ div[style*="border-left-width: 2px"]:nth-child(3) { border-color: #2e7d32 !important; } /* Green */ div[style*="border-left-width: 2px"]:nth-child(4) { border-color: #ef6c00 !important; } /* Orange */ div[style*="border-left-width: 2px"]:nth-child(5) { border-color: #c62828 !important; } /* Red */ div[style*="border-left-width: 2px"]:nth-child(6) { border-color: #00796b !important; } /* Teal */ div[style*="border-left-width: 2px"]:nth-child(7) { border-color: #c2185b !important; } /* Pink */ div[style*="border-left-width: 2px"]:nth-child(8) { border-color: #ffa000 !important; } /* Amber */ div[style*="border-left-width: 2px"]:nth-child(9) { border-color: #1565c0 !important; } /* Dark Blue */ div[style*="border-left-width: 2px"]:nth-child(10) { border-color: #6a1b9a !important; } /* Deep Purple */ div[style*="border-left-width: 2px"]:nth-child(11) { border-color: #558b2f !important; } /* Light Green */ div[style*="border-left-width: 2px"]:nth-child(12) { border-color: #d84315 !important; } /* Deep Orange */ div[style*="border-left-width: 2px"]:nth-child(13) { border-color: #303f9f !important; } /* Indigo */ div[style*="border-left-width: 2px"]:nth-child(14) { border-color: #b71c1c !important; } /* Dark Red */ div[style*="border-left-width: 2px"]:nth-child(15) { border-color: #006064 !important; } /* Cyan */ /* Collapse button styles */ .thread-collapse-btn { cursor: pointer; width: 20px; height: 20px; position: absolute; left: -16px; top: 18px; background-color: #1e2937; color: #aebbc9; border: 1px solid #4a6179; border-radius: 25%; z-index: 100; padding: 0; transition: background-color 0.2s ease; } .thread-collapse-btn:hover { background-color: #2e4054; } /* Indicator styles */ .thread-collapse-indicator { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-family: monospace; font-size: 16px; line-height: 1; user-select: none; } /* Collapsed thread styles */ .thread-collapsed { display: none !important; } /* Post container relative positioning for collapse button */ .post-with-collapse { position: relative; } /* Animation for button spin */ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .thread-collapse-btn.spinning { animation: spin 0.2s ease-in-out; } `); // Utility function to check if we're on a post page function isPostPage() { return window.location.pathname.match(/^\/profile\/[^\/]+\/post\/.+/); } function getIndentCount(postContainer) { const parent = postContainer.parentElement; if (!parent) return 0; const indents = Array.from(parent.parentElement.children).filter(child => child.getAttribute('style')?.includes('border-left-width: 2px') ); return indents.length; } function hasChildThreads(postContainer) { const currentIndents = getIndentCount(postContainer); const threadContainer = postContainer.closest('[data-thread-container]') || postContainer.parentElement?.parentElement?.parentElement?.parentElement; if (!threadContainer) return false; const nextThreadContainer = threadContainer.nextElementSibling; if (!nextThreadContainer) return false; const nextPost = nextThreadContainer.querySelector('div[role="link"][tabindex="0"]'); if (!nextPost) return false; const nextIndents = getIndentCount(nextPost); return nextIndents > currentIndents; } function toggleThread(threadStart, isCollapsed) { const currentIndents = getIndentCount(threadStart); const threadContainer = threadStart.closest('[data-thread-container]') || threadStart.parentElement?.parentElement?.parentElement?.parentElement; let nextContainer = threadContainer?.nextElementSibling; while (nextContainer) { const nextPost = nextContainer.querySelector('div[role="link"][tabindex="0"]'); if (nextPost) { const nextIndents = getIndentCount(nextPost); if (nextIndents <= currentIndents) break; if (isCollapsed) { nextContainer.classList.add('thread-collapsed'); } else { nextContainer.classList.remove('thread-collapsed'); } } nextContainer = nextContainer.nextElementSibling; } } function addCollapseButton(postContainer) { if (!postContainer || postContainer.querySelector('.thread-collapse-btn')) { return; } const button = document.createElement('button'); button.className = 'thread-collapse-btn'; button.setAttribute('aria-label', 'Collapse thread'); const indicator = document.createElement('div'); indicator.className = 'thread-collapse-indicator'; indicator.textContent = '-'; button.appendChild(indicator); postContainer.classList.add('post-with-collapse'); postContainer.appendChild(button); button.addEventListener('click', (e) => { e.stopPropagation(); const isCollapsed = button.classList.toggle('collapsed'); button.classList.add('spinning'); setTimeout(() => { indicator.textContent = isCollapsed ? '+' : '-'; button.classList.remove('spinning'); }, 200); toggleThread(postContainer, isCollapsed); }); } function initializeThreadCollapse() { if (!isPostPage()) return false; const posts = document.querySelectorAll('div[role="link"][tabindex="0"]'); let hasAddedButtons = false; posts.forEach(post => { if (hasChildThreads(post)) { addCollapseButton(post); hasAddedButtons = true; } }); return hasAddedButtons; } // Enhanced initialization with retry mechanism function initializeWithRetry() { const maxAttempts = 10; let attempts = 0; let initialized = false; function attempt() { if (attempts >= maxAttempts || initialized) return; attempts++; // Check if the main post container is present const mainPost = document.querySelector('div[role="link"][tabindex="0"]'); if (!mainPost) { setTimeout(attempt, 500); return; } // Try to initialize initialized = initializeThreadCollapse(); if (!initialized) { setTimeout(attempt, 500); } } // Start the first attempt attempt(); } // Initialize on page load initializeWithRetry(); // Set up observer for dynamic content changes const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { const posts = node.querySelectorAll('div[role="link"][tabindex="0"]'); if (posts.length > 0) { initializeWithRetry(); break; } } } } }); // Start observing after a short delay to ensure the page is ready setTimeout(() => { observer.observe(document.body, { childList: true, subtree: true }); }, 1000); })();