YouTube Video Notes

Adds a note-taking feature on YouTube videos, storing notes per video using localStorage and Tampermonkey storage.

  1. // ==UserScript==
  2. // @name YouTube Video Notes
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.9
  5. // @description Adds a note-taking feature on YouTube videos, storing notes per video using localStorage and Tampermonkey storage.
  6. // @author You
  7. // @match https://www.youtube.com/*
  8. // @grant GM_setValue
  9. // @grant GM_deleteValue
  10. // @run-at document-idle
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. let lastVideoId = null;
  17.  
  18. function saveNote(id, note) {
  19. localStorage.setItem(id, note);
  20. GM_setValue(id, note);
  21. }
  22.  
  23. function deleteNote(id) {
  24. localStorage.removeItem(id);
  25. GM_deleteValue(id);
  26. }
  27.  
  28. function createButton(id, text, displayStyle, clickHandler, customStyles = '') {
  29. const button = document.createElement('button');
  30. button.id = id;
  31. button.innerText = text;
  32. button.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--filled yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-s';
  33. button.style.cssText = `margin-top: 10px; display: ${displayStyle}; ${customStyles}`;
  34. button.addEventListener('click', clickHandler);
  35. return button;
  36. }
  37.  
  38. function createNoteUI(videoId, existingNote) {
  39. const container = document.createElement('div');
  40. container.className = 'yt-note-container item style-scope ytd-watch-metadata';
  41. container.style.cssText = `
  42. background: var(--yt-spec-badge-chip-background);
  43. width: 100%;
  44. border-radius: 12px;
  45. font-size: 14px;
  46. padding: 8px;
  47. box-sizing: border-box;
  48. `;
  49.  
  50. const noteArea = document.createElement('textarea');
  51. noteArea.id = `yt-note-textarea-${videoId}`;
  52. noteArea.style.cssText = `
  53. width: 100%;
  54. height: 80px;
  55. font-family: inherit;
  56. resize: vertical;
  57. display: ${existingNote ? 'block' : 'none'};
  58. box-sizing: border-box;
  59. padding: 4px;
  60. color: #fff;
  61. background: transparent;
  62. border-radius: 4px;
  63. border: 1px solid white;
  64. `;
  65. noteArea.value = existingNote || '';
  66. noteArea.disabled = !existingNote;
  67.  
  68. const button = createButton(`yt-note-button-${videoId}`, existingNote ? 'Edit Note' : 'Write Note', 'inline-block', () => {
  69. noteArea.style.display = 'block';
  70. noteArea.disabled = false;
  71. saveButton.style.display = 'inline-block';
  72. deleteButton.style.display = 'inline-block';
  73. button.style.display = 'none';
  74. });
  75.  
  76. const saveButton = createButton(`yt-note-save-${videoId}`, 'Save', existingNote ? 'none' : 'inline-block', () => {
  77. const noteText = noteArea.value.trim();
  78. if (noteText) {
  79. saveNote(`yt-note-${videoId}`, noteText);
  80. button.innerText = 'Edit Note';
  81. button.style.display = 'inline-block';
  82. saveButton.style.display = 'none';
  83. deleteButton.style.display = 'inline-block';
  84. noteArea.disabled = true;
  85. }
  86. }, 'margin-left: 6px;');
  87.  
  88. const deleteButton = createButton(`yt-note-delete-${videoId}`, 'Delete Note', existingNote ? 'inline-block' : 'none', () => {
  89. deleteNote(`yt-note-${videoId}`);
  90. noteArea.value = '';
  91. button.innerText = 'Write Note';
  92. button.style.display = 'inline-block';
  93. saveButton.style.display = 'none';
  94. deleteButton.style.display = 'none';
  95. noteArea.disabled = true;
  96. }, 'margin-left: 6px;');
  97.  
  98. const fetchAllNotesButton = createButton('yt-fetch-notes', 'Show All Notes', 'block', fetchAllNotes, 'margin-top: 10px; width: 100%;');
  99.  
  100. container.appendChild(noteArea);
  101. container.appendChild(button);
  102. container.appendChild(saveButton);
  103. container.appendChild(deleteButton);
  104. container.appendChild(fetchAllNotesButton);
  105.  
  106. return container;
  107. }
  108.  
  109. function insertNoteUI() {
  110. const videoId = new URL(window.location.href).searchParams.get('v');
  111. if (!videoId || videoId === lastVideoId) return;
  112.  
  113. lastVideoId = videoId;
  114.  
  115. const existingNote = localStorage.getItem(`yt-note-${videoId}`) || '';
  116. const commentSection = document.getElementById('comments');
  117. if (!commentSection) {
  118. setTimeout(insertNoteUI, 2000);
  119. return;
  120. }
  121.  
  122. const oldNote = document.querySelector('.yt-note-container');
  123. if (oldNote) oldNote.remove();
  124.  
  125. const noteUI = createNoteUI(videoId, existingNote);
  126. commentSection.parentNode.insertBefore(noteUI, commentSection);
  127. }
  128.  
  129. function fetchAllNotes() {
  130. const allNotes = {};
  131. for (let i = 0; i < localStorage.length; i++) {
  132. const key = localStorage.key(i);
  133. if (key.startsWith('yt-note-')) {
  134. allNotes[key.replace('yt-note-', '')] = localStorage.getItem(key);
  135. }
  136. }
  137. console.log(JSON.stringify(allNotes, null, 2));
  138. }
  139.  
  140. function observeNavigationChanges() {
  141. document.addEventListener('yt-navigate-finish', () => {
  142. setTimeout(insertNoteUI, 2000);
  143. });
  144.  
  145. let lastUrl = location.href;
  146. setInterval(() => {
  147. if (location.href !== lastUrl) {
  148. lastUrl = location.href;
  149. insertNoteUI();
  150. }
  151. }, 1000);
  152. }
  153.  
  154. window.addEventListener('load', () => {
  155. setTimeout(() => {
  156. insertNoteUI();
  157. observeNavigationChanges();
  158. }, 2000);
  159. });
  160.  
  161. })();