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.3
// @license MIT
// @match https://bsky.app/profile/*/post/*
// @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;
}
`);
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.parentElement.parentElement.parentElement.parentElement;
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.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); // Match the animation duration
toggleThread(postContainer, isCollapsed);
});
}
function initializeThreadCollapse() {
const posts = document.querySelectorAll('div[role="link"][tabindex="0"]');
posts.forEach(post => {
if (hasChildThreads(post)) {
addCollapseButton(post);
}
});
}
setTimeout(() => {
initializeThreadCollapse();
}, 1000);
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === 1) {
const posts = node.querySelectorAll('div[role="link"][tabindex="0"]');
if (posts.length > 0) {
initializeThreadCollapse();
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
})();