YouTube Comment to Twitter

Adds a button to YouTube watch pages to easily tweet a comment with the video link.

  1. // ==UserScript==
  2. // @name YouTube Comment to Twitter
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @description Adds a button to YouTube watch pages to easily tweet a comment with the video link.
  6. // @author torch
  7. // @match *://www.youtube.com/watch*
  8. // @grant GM_addStyle
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @run-at document-end
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use_strict';
  17.  
  18. // --- Configuration ---
  19. const BUTTON_TEXT = "🐦 Твитнуть комментарий";
  20. const POPUP_TITLE = "Написать твит о видео";
  21. const TWITTER_MAX_LENGTH = 280; // Standard Twitter limit
  22. const TWITTER_URL_LENGTH = 23; // Standard length consumed by a t.co URL
  23.  
  24. // --- Styles ---
  25. GM_addStyle(`
  26. #yt-comment-to-twitter-btn {
  27. background-color: #1DA1F2;
  28. color: white;
  29. border: none;
  30. padding: 8px 12px;
  31. text-align: center;
  32. text-decoration: none;
  33. display: inline-block;
  34. font-size: 14px;
  35. margin: 4px 2px;
  36. cursor: pointer;
  37. border-radius: 20px;
  38. font-weight: bold;
  39. }
  40. #yt-comment-to-twitter-btn:hover {
  41. background-color: #0c85d0;
  42. }
  43. .twitter-popup-overlay {
  44. position: fixed;
  45. top: 0;
  46. left: 0;
  47. width: 100%;
  48. height: 100%;
  49. background-color: rgba(0, 0, 0, 0.5);
  50. z-index: 9998;
  51. display: flex;
  52. justify-content: center;
  53. align-items: center;
  54. }
  55. .twitter-popup-content {
  56. background-color: #1e1e1e; /* Darker theme for YouTube dark mode */
  57. color: #e0e0e0;
  58. padding: 20px;
  59. border-radius: 8px;
  60. box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
  61. width: 400px;
  62. max-width: 90%;
  63. z-index: 9999;
  64. }
  65. .twitter-popup-content h3 {
  66. margin-top: 0;
  67. color: #1DA1F2;
  68. }
  69. .twitter-popup-content textarea {
  70. width: calc(100% - 20px);
  71. height: 100px;
  72. margin-bottom: 10px;
  73. padding: 10px;
  74. border: 1px solid #555;
  75. border-radius: 4px;
  76. background-color: #2a2a2a;
  77. color: #e0e0e0;
  78. resize: vertical;
  79. }
  80. .twitter-popup-content .char-counter {
  81. text-align: right;
  82. font-size: 0.9em;
  83. color: #aaa;
  84. margin-bottom: 10px;
  85. }
  86. .twitter-popup-content button {
  87. padding: 10px 15px;
  88. border: none;
  89. border-radius: 4px;
  90. cursor: pointer;
  91. font-weight: bold;
  92. margin-right: 10px;
  93. }
  94. .twitter-popup-content .tweet-btn {
  95. background-color: #1DA1F2;
  96. color: white;
  97. }
  98. .twitter-popup-content .tweet-btn:hover {
  99. background-color: #0c85d0;
  100. }
  101. .twitter-popup-content .cancel-btn {
  102. background-color: #555;
  103. color: white;
  104. }
  105. .twitter-popup-content .cancel-btn:hover {
  106. background-color: #777;
  107. }
  108. `);
  109.  
  110. let popupOverlay = null;
  111.  
  112. // getVideoTitle is not needed for the tweet content anymore, but kept in case it's useful for other features later.
  113. // function getVideoTitle() {
  114. // const titleElement = document.querySelector('h1.ytd-video-primary-info-renderer yt-formatted-string, h1.title.ytd-video-primary-info-renderer');
  115. // return titleElement ? titleElement.textContent.trim() : "YouTube Video";
  116. // }
  117.  
  118. function getVideoUrl() {
  119. return window.location.href;
  120. }
  121.  
  122. function createPopup() {
  123. if (popupOverlay) {
  124. popupOverlay.style.display = 'flex'; // Show if already created
  125. if (popupOverlay.querySelector('textarea')) {
  126. popupOverlay.querySelector('textarea').focus();
  127. }
  128. return;
  129. }
  130.  
  131. popupOverlay = document.createElement('div');
  132. popupOverlay.className = 'twitter-popup-overlay';
  133. popupOverlay.onclick = function(e) {
  134. if (e.target === popupOverlay) {
  135. closePopup();
  136. }
  137. };
  138.  
  139. const popupContent = document.createElement('div');
  140. popupContent.className = 'twitter-popup-content';
  141.  
  142. const title = document.createElement('h3');
  143. title.textContent = POPUP_TITLE;
  144.  
  145. const textarea = document.createElement('textarea');
  146. textarea.placeholder = "Ваш комментарий...";
  147.  
  148. const charCounter = document.createElement('div');
  149. charCounter.className = 'char-counter';
  150. const updateCharCounter = () => {
  151. // The tweet will consist of the comment, a space, and the URL.
  152. // Twitter uses t.co to shorten URLs, which takes up a fixed number of characters.
  153. const lengthOfComment = textarea.value.length;
  154. const lengthOfSpaceAndUrl = (lengthOfComment > 0 ? 1 : 0) + TWITTER_URL_LENGTH; // Add space only if comment exists
  155. const remaining = TWITTER_MAX_LENGTH - lengthOfComment - lengthOfSpaceAndUrl;
  156. charCounter.textContent = `${remaining} символов осталось`;
  157. charCounter.style.color = remaining < 0 ? 'red' : '#aaa';
  158. };
  159. textarea.addEventListener('input', updateCharCounter);
  160.  
  161.  
  162. const tweetButton = document.createElement('button');
  163. tweetButton.textContent = "Твитнуть";
  164. tweetButton.className = 'tweet-btn';
  165. tweetButton.onclick = function() {
  166. const comment = textarea.value.trim();
  167. // Comment can be empty, in which case only the URL is tweeted via the 'url' parameter.
  168. // Twitter usually pre-fills the text field with the URL if the text parameter is empty.
  169.  
  170. const videoUrl = getVideoUrl();
  171.  
  172. // Construct tweet text: only the comment.
  173. // The videoUrl will be passed in the 'url' parameter of the Twitter intent.
  174. // Twitter will append the URL to the comment text.
  175. let tweetText = comment;
  176.  
  177. const twitterIntentUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(videoUrl)}`;
  178.  
  179. window.open(twitterIntentUrl, '_blank');
  180. closePopup();
  181. };
  182.  
  183. const cancelButton = document.createElement('button');
  184. cancelButton.textContent = "Отмена";
  185. cancelButton.className = 'cancel-btn';
  186. cancelButton.onclick = closePopup;
  187.  
  188. popupContent.appendChild(title);
  189. popupContent.appendChild(textarea);
  190. popupContent.appendChild(charCounter);
  191. popupContent.appendChild(tweetButton);
  192. popupContent.appendChild(cancelButton);
  193. popupOverlay.appendChild(popupContent);
  194. document.body.appendChild(popupOverlay);
  195.  
  196. // Initialize counter
  197. updateCharCounter();
  198. textarea.focus();
  199. }
  200.  
  201. function closePopup() {
  202. if (popupOverlay) {
  203. // Clear textarea for next time
  204. const textarea = popupOverlay.querySelector('textarea');
  205. if (textarea) {
  206. textarea.value = '';
  207. }
  208. popupOverlay.style.display = 'none';
  209. }
  210. }
  211.  
  212. function addButton() {
  213. // More robust selectors for YouTube's dynamic layout
  214. const commonActionSelectors = [
  215. '#actions-inner #menu', // Older layout under video
  216. '#menu-container.ytd-watch-metadata', // Older layout alternative
  217. 'ytd-video-actions #actions', // Newer layout for like/dislike etc.
  218. '#actions.ytd-watch-flexy' // Common actions row
  219. ];
  220. const fallbackSelectors = [
  221. '#info-contents #top-row.ytd-watch-info-text',
  222. '#meta-contents #info-contents',
  223. '#meta-contents #info',
  224. '#owner #subscribe-button' // As a last resort, place it near subscribe
  225. ];
  226.  
  227. let actionsContainer = null;
  228. for (const selector of commonActionSelectors) {
  229. actionsContainer = document.querySelector(selector);
  230. if (actionsContainer) break;
  231. }
  232.  
  233. if (!actionsContainer) {
  234. for (const selector of fallbackSelectors) {
  235. actionsContainer = document.querySelector(selector);
  236. if (actionsContainer) break;
  237. }
  238. }
  239.  
  240. if (actionsContainer) {
  241. if (actionsContainer.querySelector('#yt-comment-to-twitter-btn')) {
  242. return; // Button already exists
  243. }
  244.  
  245. const twitterButton = document.createElement('button');
  246. twitterButton.id = 'yt-comment-to-twitter-btn';
  247. twitterButton.textContent = BUTTON_TEXT;
  248. twitterButton.onclick = createPopup;
  249.  
  250. // Attempt to insert it in a reasonable place
  251. if (actionsContainer.id === 'actions' && actionsContainer.parentElement?.tagName === 'YTD-VIDEO-ACTIONS') {
  252. // Preferred: Add next to like/share buttons
  253. actionsContainer.insertBefore(twitterButton, actionsContainer.children[Math.min(2, actionsContainer.children.length)]);
  254. } else if (actionsContainer.firstChild) {
  255. actionsContainer.insertBefore(twitterButton, actionsContainer.firstChild.nextSibling);
  256. } else {
  257. actionsContainer.appendChild(twitterButton);
  258. }
  259. // console.log("YouTube Comment to Twitter button added to:", actionsContainer);
  260. } else {
  261. // console.warn("Could not find a suitable container for the Twitter button after multiple attempts.");
  262. }
  263. }
  264.  
  265. // YouTube uses dynamic loading, so we need to observe DOM changes
  266. function observeDOM() {
  267. const targetNode = document.body;
  268. const config = { childList: true, subtree: true };
  269. let lastPathname = window.location.pathname;
  270. let debounceTimer;
  271.  
  272. const handleMutation = () => {
  273. // Try to add the button if it's not there
  274. if (!document.querySelector('#yt-comment-to-twitter-btn')) {
  275. addButton();
  276. }
  277. };
  278.  
  279. const callback = function(mutationsList, observer) {
  280. // Check if navigation has happened to a new watch page
  281. if (window.location.pathname !== lastPathname && window.location.pathname.includes("/watch")) {
  282. lastPathname = window.location.pathname;
  283. // Wait a bit for the new page to load elements
  284. clearTimeout(debounceTimer);
  285. debounceTimer = setTimeout(handleMutation, 1000);
  286. return;
  287. }
  288.  
  289. // General check for dynamic content loading on the current page
  290. for (const mutation of mutationsList) {
  291. if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
  292. // Check if a potential container is now available
  293. if (document.querySelector('#actions-inner #menu, #menu-container.ytd-watch-metadata, ytd-video-actions #actions, #actions.ytd-watch-flexy') && !document.querySelector('#yt-comment-to-twitter-btn')) {
  294. clearTimeout(debounceTimer);
  295. debounceTimer = setTimeout(handleMutation, 300); // Debounce to avoid multiple rapid adds
  296. break;
  297. }
  298. }
  299. }
  300. };
  301.  
  302. const observer = new MutationObserver(callback);
  303. observer.observe(targetNode, config);
  304.  
  305. // Initial attempt in case the element is already there
  306. if (window.location.pathname.includes("/watch")) {
  307. setTimeout(addButton, 1000); // Initial delay for page load
  308. }
  309. }
  310.  
  311. // Make sure the script runs after the page is mostly loaded
  312. if (document.readyState === "complete" || document.readyState === "interactive") {
  313. observeDOM();
  314. } else {
  315. window.addEventListener('DOMContentLoaded', observeDOM);
  316. }
  317.  
  318. })();