Twitter ALT info

在Twitter信息流中显示图片的ALT信息

当前为 2023-04-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Twitter ALT info
  3. // @namespace https://twitter.com/shangrenxi
  4. // @version 1.0.1
  5. // @description 在Twitter信息流中显示图片的ALT信息
  6. // @icon http://www.google.com/s2/favicons?domain=twitter.com
  7. // @author Alban
  8. // @match https://twitter.com/*
  9. // @grant GM_addStyle
  10. // @license MIT Alban
  11. // ==/UserScript==
  12.  
  13. const tweetSelector = 'article[data-testid="tweet"]';
  14. const tweetTextSelector = 'div[data-testid="tweetText"]';
  15. const tweetPhotoSelector = 'div[data-testid^="tweetPhoto"]';
  16.  
  17. const appendAltText = (tweet, altText) => {
  18. const tweetTextElement = tweet.querySelector(tweetTextSelector);
  19. const existingList =
  20. tweetTextElement?.querySelector(".alt-list") ??
  21. (() => {
  22. const newList = document.createElement("ol");
  23. newList.className = "alt-list";
  24. const altPromptText = document.createTextNode("ALTs: ");
  25. const altPrompt = document.createElement("span");
  26. altPrompt.className = "alt-prompt";
  27. altPrompt.appendChild(altPromptText);
  28. const container = document.createElement("div");
  29. container.className = "alt-container";
  30. container.appendChild(altPrompt);
  31. container.appendChild(newList);
  32. tweetTextElement.appendChild(container);
  33. return newList;
  34. })();
  35.  
  36. const listItem = document.createElement("li");
  37. const altTextNode = document.createTextNode(altText);
  38. const altTextContainer = document.createElement("div");
  39. altTextContainer.className = "alt-text"
  40. altTextContainer.appendChild(altTextNode);
  41. listItem.appendChild(altTextContainer);
  42. existingList.appendChild(listItem);
  43. };
  44.  
  45. const processedTweets = new Map();
  46.  
  47. const processTweet = (tweet) => {
  48. if (processedTweets.get(tweet)) {
  49. return;
  50. }
  51. processedTweets.set(tweet, true);
  52. tweet.querySelectorAll(tweetPhotoSelector).forEach((photo) => {
  53. const altText = photo.getAttribute("aria-label");
  54. if (altText && altText.length >= 10) {
  55. appendAltText(tweet, altText);
  56. }
  57. });
  58. };
  59.  
  60. const intersectionObserver = new IntersectionObserver(
  61. (entries) => {
  62. entries.forEach((entry) => {
  63. if (
  64. entry.isIntersecting &&
  65. !processedTweets.get(entry.target) &&
  66. entry.target.matches(tweetSelector) &&
  67. entry.target.querySelector(tweetPhotoSelector)
  68. ) {
  69. processTweet(entry.target);
  70. }
  71. });
  72. },
  73. { threshold: 0.5 }
  74. );
  75.  
  76. const observerConfig = { childList: true, subtree: true };
  77. const mutationObserver = new MutationObserver((mutationsList) =>
  78. mutationsList.forEach(({ addedNodes }) =>
  79. addedNodes.forEach((node) => {
  80. if (node.nodeType === Node.ELEMENT_NODE) {
  81. node.querySelectorAll(tweetSelector).forEach((tweet) => {
  82. intersectionObserver.observe(tweet);
  83. });
  84. }
  85. })
  86. )
  87. );
  88.  
  89. document.querySelectorAll(tweetSelector).forEach((tweet) => {
  90. intersectionObserver.observe(tweet);
  91. });
  92.  
  93. mutationObserver.observe(document.body, observerConfig);
  94.  
  95. GM_addStyle(`
  96. .alt-container {
  97. display: block;
  98. margin-top: 10px;
  99. background-color: #cce6ff5e;
  100. padding: 8px;
  101. border-radius: 8px;
  102. border-style: solid;
  103. border-color: #cce6ff;
  104. }
  105.  
  106. .alt-prompt {
  107. font-weight: bold;
  108. color: #1d9bf0;
  109. font-size: 15px;
  110. }
  111.  
  112. .alt-list {
  113. padding-left: 20px;
  114. margin-block-start: 0.2em;
  115. margin-block-end: 0.2em;
  116. }
  117.  
  118. .alt-text {
  119. padding: .2em;
  120. font-size: 15px;
  121. }
  122. `);