YT 直播聊天室贴图复制工具

让 YouTube™ 直播聊天室的贴图可以被复制

  1. // ==UserScript==
  2. // @name YT Live Chat Emoji Copy Tool
  3. // @name:zh YT 直播聊天室貼圖複製工具
  4. // @name:zh-TW YT 直播聊天室貼圖複製工具
  5. // @name:zh-CN YT 直播聊天室贴图复制工具
  6. // @namespace https://github.com/kevin823lin
  7. // @version 0.3
  8. // @description Make YouTube™ Live Chat's emoji can be copied.
  9. // @description:zh 讓 YouTube™ 直播聊天室的貼圖可以被複製
  10. // @description:zh-TW 讓 YouTube™ 直播聊天室的貼圖可以被複製
  11. // @description:zh-CN 让 YouTube™ 直播聊天室的贴图可以被复制
  12. // @author kevin823lin
  13. // @match https://www.youtube.com/live_chat*
  14. // @match https://www.youtube.com/live_chat_replay*
  15. // @match https://studio.youtube.com/live_chat*
  16. // @match https://studio.youtube.com/live_chat_replay*
  17. // @icon https://www.google.com/s2/favicons?domain=youtube.com
  18. // @require https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js
  19. // @grant none
  20. // @date 2025-01-11
  21. // @license MIT
  22. // ==/UserScript==
  23.  
  24. /*!
  25. * YT Live Chat Emoji Copy Tool v0.3
  26. * https://github.com/kevin823lin/yt-live-chat-emoji-copy-tool
  27. *
  28. * Includes Lodash v4.17.21
  29. *
  30. * Copyright (c) 2025 kevin823lin
  31. * Released under the MIT license
  32. * https://opensource.org/licenses/MIT
  33. *
  34. * Date: 2025-01-11
  35. */
  36.  
  37. (function() {
  38. 'use strict';
  39.  
  40. // Your code here...
  41. const TYPES = {
  42. CHAT: Symbol("Chat"),
  43. INPUT: Symbol("Input"),
  44. PICKER: Symbol("Picker"),
  45. };
  46.  
  47. const YOUTUBE_CHANNEL_ID_REGEX = /^UCkszU2WH9gy1mb0dV-11UJg\//;
  48. const CHAT_EMOJI_SELECTOR = 'img.emoji.yt-live-chat-text-message-renderer[shared-tooltip-text][data-emoji-id]:not(.copyable)';
  49. const INPUT_FIELD_SELECTOR = 'yt-live-chat-text-input-field-renderer > #input';
  50. const EMOJI_PICKER_SELECTOR = 'div.yt-emoji-picker-renderer#categories';
  51. const VERSION = (typeof GM_info !== 'undefined') ? GM_info?.script?.version : (typeof chrome !== 'undefined') ? chrome?.runtime?.getManifest()?.version : '';
  52.  
  53. /**
  54. * Check if the emoji is already copyable.
  55. */
  56. function isEmojiCopyable(emoji) {
  57. return emoji.classList.contains('copyable');
  58. }
  59.  
  60. /**
  61. * Retrieve the emoji's ID.
  62. */
  63. function getEmojiId(emoji) {
  64. return emoji.dataset.emojiId || emoji.id;
  65. }
  66.  
  67. /**
  68. * Determine if the emoji is a YouTube-specific emoji.
  69. */
  70. function isYoutubeEmoji(emoji) {
  71. const id = getEmojiId(emoji);
  72. return !id || YOUTUBE_CHANNEL_ID_REGEX.test(id);
  73. }
  74.  
  75. /**
  76. * Update the emoji's alt attribute with colon format.
  77. */
  78. function updateEmojiAltWithColon(emoji, compareText = null) {
  79. if (compareText && !compareText.match(emoji.alt)) return;
  80. emoji.alt = `:${isYoutubeEmoji(emoji) ? '' : '_'}${emoji.alt}:`;
  81. }
  82.  
  83. /**
  84. * Process the emoji based on its type.
  85. */
  86. function processEmoji(emoji, type) {
  87. if (isEmojiCopyable(emoji)) return;
  88.  
  89. emoji.classList.add('copyable');
  90. let compareText = null;
  91.  
  92. switch (type) {
  93. case TYPES.CHAT:
  94. if (!document.contains(emoji)) return;
  95. compareText = emoji.getAttribute('shared-tooltip-text');
  96. break;
  97. case TYPES.INPUT:
  98. if (!getEmojiId(emoji)) return;
  99. break;
  100. case TYPES.PICKER:
  101. compareText = emoji.getAttribute('aria-label');
  102. break;
  103. default:
  104. console.warn(`Unknown emoji type: ${type}`);
  105. return;
  106. }
  107.  
  108. updateEmojiAltWithColon(emoji, compareText);
  109. }
  110.  
  111. /**
  112. * Update all emojis in the selected range.
  113. */
  114. function updateSelectedRangeEmojis() {
  115. try {
  116. const selection = window.getSelection();
  117. if (!selection.rangeCount) return;
  118. const range = selection.getRangeAt(0);
  119. const fragment = range.cloneContents();
  120. const selectedEmojis = fragment.querySelectorAll(CHAT_EMOJI_SELECTOR);
  121. selectedEmojis.forEach(clonedEmoji => {
  122. const originalEmoji = range.commonAncestorContainer.querySelector(`img.emoji#${clonedEmoji.id}`);
  123. if (originalEmoji) {
  124. processEmoji(originalEmoji, TYPES.CHAT);
  125. }
  126. });
  127. } catch (error) {
  128. console.error(`Error in updateSelectedRangeEmojis: ${error}`);
  129. }
  130. }
  131.  
  132. /**
  133. * Update all emojis inside the input field.
  134. */
  135. function updateInputFieldEmojis(inputField) {
  136. const inputEmojis = inputField?.getElementsByClassName('yt-live-chat-text-input-field-renderer') || [];
  137. Array.from(inputEmojis).forEach((node) => processEmoji(node, TYPES.INPUT));
  138. }
  139.  
  140. /**
  141. * Update emojis in the emoji picker based on mutations.
  142. */
  143. function updateEmojiPickerEmojis(mutations) {
  144. mutations.forEach((mutation) => {
  145. mutation.addedNodes.forEach((node) => {
  146. if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'IMG') {
  147. processEmoji(node, TYPES.PICKER);
  148. }
  149. });
  150. });
  151. }
  152.  
  153. /**
  154. * Initialize observers and event listeners on page load.
  155. */
  156. function initializeObservers() {
  157. const inputField = document.querySelector(INPUT_FIELD_SELECTOR);
  158. const emojiPicker = document.querySelector(EMOJI_PICKER_SELECTOR);
  159.  
  160. const selectionchangeCallback = _.debounce(updateSelectedRangeEmojis, 200);
  161. const observeInputFieldCallback = _.debounce(() => {
  162. if (inputField) updateInputFieldEmojis(inputField);
  163. }, 200);
  164. const observeEmojiPickerCallback = updateEmojiPickerEmojis;
  165.  
  166. const inputFieldObserver = new MutationObserver(observeInputFieldCallback);
  167. const emojiPickerObserver = new MutationObserver(observeEmojiPickerCallback);
  168.  
  169. if (inputField) inputFieldObserver.observe(inputField, { childList: true, subtree: true });
  170. if (emojiPicker) emojiPickerObserver.observe(emojiPicker, { childList: true, subtree: true });
  171.  
  172. document.addEventListener('selectionchange', selectionchangeCallback);
  173. }
  174.  
  175. if (/^\/live_chat/.test(window.location.pathname)) {
  176. initializeObservers();
  177. console.log(`[YT Live Chat Emoji Copy Tool${VERSION ? ` v${VERSION}` : ''}]`);
  178. }
  179. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址