YouTube Channel Scroll Saver

Saves and restores scroll position on YouTube channel videos pages

当前为 2025-04-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Channel Scroll Saver
  3. // @name:de YouTube Channel Scroll Saver
  4. // @namespace https://www.youtube.com/
  5. // @version 1.6.1
  6. // @description Saves and restores scroll position on YouTube channel videos pages
  7. // @description:de Speichert und stellt die Scrollposition auf der Video-Seite des YouTube-Kanals wieder her
  8. // @author Kamikaze (https://github.com/Kamiikaze)
  9. // @supportURL https://github.com/Kamiikaze/Tampermonkey/issues
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
  11. // @match https://www.youtube.com/@*/videos
  12. // @match https://www.youtube.com/*
  13. // @require https://greasyfork.org/scripts/455253-kamikaze-script-utils/code/Kamikaze'%20Script%20Utils.js
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js
  15. // @resource toastifyCss https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
  16. // @grant GM_xmlhttpRequest
  17. // @grant GM_getResourceText
  18. // @grant GM_addStyle
  19. // @license MIT
  20. // ==/UserScript==
  21.  
  22. // Autoscroll to last postion on this channel.
  23. // Leave it false if you wan to click a button to manual scroll
  24. const doAutoscroll = false
  25.  
  26. // Save scroll position at most every 3 second
  27. const saveDelay = 3000
  28.  
  29.  
  30.  
  31. /* global Logger waitForElm notify */
  32.  
  33.  
  34.  
  35. const SCRIPT_NAME = "YT Scroll Saver"
  36. const log = new Logger(SCRIPT_NAME, 4);
  37.  
  38. // Load remote JS
  39. GM_xmlhttpRequest({
  40. method : "GET",
  41. // from other domain than the @match one (.org / .com):
  42. url : "https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js",
  43. onload : (ev) =>
  44. {
  45. let e = document.createElement('script');
  46. e.innerText = ev.responseText;
  47. document.head.appendChild(e);
  48. }
  49. });
  50.  
  51. // Load remote CSS
  52. const extCss = GM_getResourceText("toastifyCss");
  53. GM_addStyle(extCss);
  54.  
  55.  
  56. (function() {
  57. // https://stackoverflow.com/questions/61964265/getting-error-this-document-requires-trustedhtml-assignment-in-chrome
  58. if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) {
  59. window.trustedTypes.createPolicy('default', {
  60. createHTML: string => string
  61. // Optional, only needed for script (url) tags
  62. //,createScriptURL: string => string
  63. //,createScript: string => string,
  64. });
  65. }
  66.  
  67. let isScrolling = false;
  68. let btnAdded = false;
  69. let saveTimeout = null;
  70. let lastUrl = location.href;
  71.  
  72. function getChannelUsername() {
  73. const match = window.location.pathname.match(/@([^/]+)/);
  74. return match ? match[1] : null;
  75. }
  76.  
  77. function saveScrollPosition() {
  78. const username = getChannelUsername();
  79. if (!username) return;
  80.  
  81. const scrollPosition = window.scrollY;
  82. if ( scrollPosition > 800 ) {
  83. localStorage.setItem(`yt_scroll_${username}`, scrollPosition);
  84. notify(`[YT Scroll Saver] Saved position: ${scrollPosition}px for @${username}`, 3000)
  85. log.debug(`Saved position: ${scrollPosition}px for @${username}`);
  86. } else {
  87. log.debug(`Scroll pos is below 800 (${scrollPosition}). Don't save pos.`);
  88. return
  89. }
  90. }
  91.  
  92. function loadScrollPosition() {
  93. const username = getChannelUsername();
  94. if (!username) {
  95. isScrolling = false
  96. return
  97. };
  98.  
  99. const savedPosition = parseInt(localStorage.getItem(`yt_scroll_${username}`) || "0", 10);
  100. if (savedPosition <= 0) {
  101. isScrolling = false
  102. return
  103. };
  104.  
  105. notify(`[YT Scroll Saver] Trying to restore position: ${savedPosition}px for @${username}`)
  106. log.debug(`[YT Scroll Saver] Trying to restore position: ${savedPosition}px for @${username}`);
  107.  
  108. if (!btnAdded) createManualScrollBtn(savedPosition)
  109. if (doAutoscroll) scrollTo(savedPosition)
  110. }
  111.  
  112. function scrollTo(pos) {
  113. let attempts = 0;
  114. const maxAttempts = 20; // 500ms * 20 = 10 seconds max
  115.  
  116. const scrollInterval = setInterval(() => {
  117. if (window.scrollY >= pos || attempts >= maxAttempts) {
  118. clearInterval(scrollInterval);
  119. isScrolling = false
  120. notify(`[[YT Scroll Saver] Scroll position reached or max attempts hit. {Pos: ${window.scrollY}, Saved: ${pos}}`)
  121. log.debug(`[YT Scroll Saver] Scroll position reached or max attempts hit. {Pos: ${window.scrollY}, Saved: ${pos}}`);
  122. return;
  123. }
  124. notify(`[YT Scroll Saver] Scrolling.. `, 1000)
  125. log.debug(`[YT Scroll Saver] Scrolling.. `);
  126. window.scrollTo(0, pos);
  127. attempts++;
  128. }, 500);
  129. }
  130.  
  131. async function createManualScrollBtn(pos) {
  132. const chipList = await waitForElm("iron-selector")
  133. const btn = document.createElement("button")
  134.  
  135. btn.addEventListener( 'click', () => scrollTo(pos) )
  136. btn.innerText = `Scroll to: ${pos}`
  137. btn.style = `
  138. background-color: transparent;
  139. padding: 8px;
  140. border-radius: 10px;
  141. margin: 0 30px;
  142. color: #f1f1f1;
  143. border-color: rgba(255, 255, 255, 0.2);
  144. outline: none !important;
  145. cursor: pointer;
  146. `
  147.  
  148. chipList.append(btn)
  149. btnAdded = true
  150. }
  151.  
  152. function checkUrlChange() {
  153. if (location.href !== lastUrl) {
  154. lastUrl = location.href;
  155. if (window.location.pathname.includes('/videos')) {
  156. log.debug("[YT Scroll Saver] Detected navigation to a channel's /videos page.");
  157. isScrolling = true
  158. setTimeout(loadScrollPosition, 1000);
  159. } else {
  160. btnAdded = false
  161. }
  162. log.debug("btnAdded",btnAdded)
  163. }
  164. }
  165.  
  166. // Attach scroll event listener
  167. window.addEventListener('scroll', () => {
  168. if (!window.location.pathname.includes('/videos') || isScrolling) return;
  169. if (saveTimeout) clearTimeout(saveTimeout);
  170. saveTimeout = setTimeout(saveScrollPosition, saveDelay);
  171. });
  172.  
  173. // Watch for SPA navigation changes
  174. const observer = new MutationObserver(checkUrlChange);
  175. observer.observe(document.body, { childList: true, subtree: true });
  176.  
  177. // Restore scroll position after page loads
  178. if (window.location.pathname.includes('/videos')) {
  179. isScrolling = true
  180. notify(`[YT Scroll Saver] Loading scroll position.`)
  181. log.debug(`[YT Scroll Saver] Loading scroll position.`);
  182. setTimeout(loadScrollPosition, 1000); // Delay to allow initial content to load
  183. };
  184. })();