Bluesky Threading Improvements

Adds colors and expand/collapse functionality to Bluesky threads

当前为 2024-11-21 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Bluesky Threading Improvements
  3. // @namespace zetaphor.com
  4. // @description Adds colors and expand/collapse functionality to Bluesky threads
  5. // @version 0.3
  6. // @license MIT
  7. // @match https://bsky.app/profile/*/post/*
  8. // @grant GM_addStyle
  9. // ==/UserScript==
  10.  
  11. (function() {
  12. 'use strict';
  13.  
  14. // Add our styles
  15. GM_addStyle(`
  16. /* Thread depth colors */
  17. div[style*="border-left-width: 2px"] {
  18. border-left-width: 2px !important;
  19. border-left-style: solid !important;
  20. }
  21.  
  22. /* Color definitions */
  23. div[style*="border-left-width: 2px"]:nth-child(1) { border-color: #2962ff !important; } /* Blue */
  24. div[style*="border-left-width: 2px"]:nth-child(2) { border-color: #8e24aa !important; } /* Purple */
  25. div[style*="border-left-width: 2px"]:nth-child(3) { border-color: #2e7d32 !important; } /* Green */
  26. div[style*="border-left-width: 2px"]:nth-child(4) { border-color: #ef6c00 !important; } /* Orange */
  27. div[style*="border-left-width: 2px"]:nth-child(5) { border-color: #c62828 !important; } /* Red */
  28. div[style*="border-left-width: 2px"]:nth-child(6) { border-color: #00796b !important; } /* Teal */
  29. div[style*="border-left-width: 2px"]:nth-child(7) { border-color: #c2185b !important; } /* Pink */
  30. div[style*="border-left-width: 2px"]:nth-child(8) { border-color: #ffa000 !important; } /* Amber */
  31. div[style*="border-left-width: 2px"]:nth-child(9) { border-color: #1565c0 !important; } /* Dark Blue */
  32. div[style*="border-left-width: 2px"]:nth-child(10) { border-color: #6a1b9a !important; } /* Deep Purple */
  33. div[style*="border-left-width: 2px"]:nth-child(11) { border-color: #558b2f !important; } /* Light Green */
  34. div[style*="border-left-width: 2px"]:nth-child(12) { border-color: #d84315 !important; } /* Deep Orange */
  35. div[style*="border-left-width: 2px"]:nth-child(13) { border-color: #303f9f !important; } /* Indigo */
  36. div[style*="border-left-width: 2px"]:nth-child(14) { border-color: #b71c1c !important; } /* Dark Red */
  37. div[style*="border-left-width: 2px"]:nth-child(15) { border-color: #006064 !important; } /* Cyan */
  38.  
  39.  
  40. /* Collapse button styles */
  41. .thread-collapse-btn {
  42. cursor: pointer;
  43. width: 20px;
  44. height: 20px;
  45. position: absolute;
  46. left: -16px;
  47. top: 18px;
  48. background-color: #1e2937;
  49. color: #aebbc9;
  50. border: 1px solid #4a6179;
  51. border-radius: 25%;
  52. z-index: 100;
  53. padding: 0;
  54. transition: background-color 0.2s ease;
  55. }
  56.  
  57. .thread-collapse-btn:hover {
  58. background-color: #2e4054;
  59. }
  60.  
  61. /* Indicator styles */
  62. .thread-collapse-indicator {
  63. position: absolute;
  64. top: 50%;
  65. left: 50%;
  66. transform: translate(-50%, -50%);
  67. font-family: monospace;
  68. font-size: 16px;
  69. line-height: 1;
  70. user-select: none;
  71. }
  72.  
  73. /* Collapsed thread styles */
  74. .thread-collapsed {
  75. display: none !important;
  76. }
  77.  
  78. /* Post container relative positioning for collapse button */
  79. .post-with-collapse {
  80. position: relative;
  81. }
  82.  
  83. /* Animation for button spin */
  84. @keyframes spin {
  85. 0% { transform: rotate(0deg); }
  86. 100% { transform: rotate(360deg); }
  87. }
  88.  
  89. .thread-collapse-btn.spinning {
  90. animation: spin 0.2s ease-in-out;
  91. }
  92. `);
  93.  
  94. function getIndentCount(postContainer) {
  95. const parent = postContainer.parentElement;
  96. if (!parent) return 0;
  97.  
  98. const indents = Array.from(parent.parentElement.children).filter(child =>
  99. child.getAttribute('style')?.includes('border-left-width: 2px')
  100. );
  101.  
  102. return indents.length;
  103. }
  104.  
  105. function hasChildThreads(postContainer) {
  106. const currentIndents = getIndentCount(postContainer);
  107. const threadContainer = postContainer.parentElement.parentElement.parentElement.parentElement;
  108. const nextThreadContainer = threadContainer.nextElementSibling;
  109.  
  110. if (!nextThreadContainer) return false;
  111.  
  112. const nextPost = nextThreadContainer.querySelector('div[role="link"][tabindex="0"]');
  113. if (!nextPost) return false;
  114.  
  115. const nextIndents = getIndentCount(nextPost);
  116. return nextIndents > currentIndents;
  117. }
  118.  
  119. function toggleThread(threadStart, isCollapsed) {
  120. const currentIndents = getIndentCount(threadStart);
  121. const threadContainer = threadStart.parentElement.parentElement.parentElement.parentElement;
  122.  
  123. let nextContainer = threadContainer.nextElementSibling;
  124. while (nextContainer) {
  125. const nextPost = nextContainer.querySelector('div[role="link"][tabindex="0"]');
  126. if (nextPost) {
  127. const nextIndents = getIndentCount(nextPost);
  128.  
  129. if (nextIndents <= currentIndents) break;
  130.  
  131. if (isCollapsed) {
  132. nextContainer.classList.add('thread-collapsed');
  133. } else {
  134. nextContainer.classList.remove('thread-collapsed');
  135. }
  136. }
  137. nextContainer = nextContainer.nextElementSibling;
  138. }
  139. }
  140.  
  141. function addCollapseButton(postContainer) {
  142. if (!postContainer || postContainer.querySelector('.thread-collapse-btn')) {
  143. return;
  144. }
  145.  
  146. const button = document.createElement('button');
  147. button.className = 'thread-collapse-btn';
  148. button.setAttribute('aria-label', 'Collapse thread');
  149.  
  150. const indicator = document.createElement('div');
  151. indicator.className = 'thread-collapse-indicator';
  152. indicator.textContent = '-';
  153. button.appendChild(indicator);
  154.  
  155. postContainer.classList.add('post-with-collapse');
  156. postContainer.appendChild(button);
  157.  
  158. button.addEventListener('click', (e) => {
  159. e.stopPropagation();
  160. const isCollapsed = button.classList.toggle('collapsed');
  161.  
  162. button.classList.add('spinning');
  163.  
  164. setTimeout(() => {
  165. indicator.textContent = isCollapsed ? '+' : '-';
  166. button.classList.remove('spinning');
  167. }, 200); // Match the animation duration
  168.  
  169. toggleThread(postContainer, isCollapsed);
  170. });
  171. }
  172.  
  173. function initializeThreadCollapse() {
  174. const posts = document.querySelectorAll('div[role="link"][tabindex="0"]');
  175.  
  176. posts.forEach(post => {
  177. if (hasChildThreads(post)) {
  178. addCollapseButton(post);
  179. }
  180. });
  181. }
  182.  
  183. setTimeout(() => {
  184. initializeThreadCollapse();
  185. }, 1000);
  186.  
  187. const observer = new MutationObserver((mutations) => {
  188. mutations.forEach((mutation) => {
  189. mutation.addedNodes.forEach(node => {
  190. if (node.nodeType === 1) {
  191. const posts = node.querySelectorAll('div[role="link"][tabindex="0"]');
  192. if (posts.length > 0) {
  193. initializeThreadCollapse();
  194. }
  195. }
  196. });
  197. });
  198. });
  199.  
  200. observer.observe(document.body, {
  201. childList: true,
  202. subtree: true
  203. });
  204. })();