YouTube - Thumbnail Anywhere

A user script to add a little functionality related to YouTube thumbnails.

  1. // ==UserScript==
  2. // @name YouTube - Thumbnail Anywhere
  3. // @name:ja YouTube - サムネイルといっしょ
  4. // @description A user script to add a little functionality related to YouTube thumbnails.
  5. // @description:ja YouTubeのサムネイルに関するちょっとした機能を追加するユーザースクリプトです。
  6. // @version 1.0.1
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=www.youtube.com
  8. // @match https://www.youtube.com/*
  9. // @namespace https://github.com/sqrtox/yt-thumbnail-anywhere
  10. // @author sqrtox
  11. // @license MIT
  12. // @grant none
  13. // @runAt document-end
  14. // ==/UserScript==
  15.  
  16. // NOTE: This file was built by esbuild
  17.  
  18. (async () => {
  19. "use strict";
  20.  
  21. // src/css.ts
  22. var CSS_CLASS_PREFIX = "userscript-";
  23. var createCssClasses = (...classes) => {
  24. const result = {};
  25. for (const class_ of classes) {
  26. result[class_] =
  27. `${CSS_CLASS_PREFIX}${Math.random().toString(36).slice(2)}`;
  28. }
  29. return result;
  30. };
  31. var injectCss = (css) => {
  32. const style = document.createElement("style");
  33. style.setHTMLUnsafe(css);
  34. document.head.append(style);
  35. };
  36.  
  37. // src/image.ts
  38. var loadImage = (src) => {
  39. const { promise, resolve, reject } = Promise.withResolvers();
  40. const events = new AbortController();
  41. const image = document.createElement("img");
  42. image.addEventListener(
  43. "load",
  44. () => {
  45. events.abort();
  46. resolve(image);
  47. },
  48. {
  49. signal: events.signal,
  50. },
  51. );
  52. image.addEventListener(
  53. "error",
  54. () => {
  55. events.abort();
  56. reject();
  57. },
  58. {
  59. signal: events.signal,
  60. },
  61. );
  62. image.src = src;
  63. return promise;
  64. };
  65.  
  66. // src/thumbnail.ts
  67. var applySmallThumbnail = async (watchId, largeThumbnail2) => {
  68. const classes = createCssClasses("title", "smallThumbnail");
  69. injectCss(`
  70. .${classes.title} {
  71. display: flex;
  72. align-items: center;
  73. column-gap: 1rem;
  74. }
  75.  
  76.  
  77. .${classes.smallThumbnail} {
  78. height: 64px;
  79. }
  80.  
  81. .${classes.smallThumbnail}:hover {
  82. position: relative;
  83. }
  84.  
  85. .${classes.smallThumbnail}:hover::after {
  86. position: absolute;
  87. cursor: zoom-in;
  88. top: 0;
  89. left: 0;
  90. content: "";
  91. backdrop-filter: brightness(0.75);
  92. width: 100%;
  93. height: 100%;
  94. }
  95.  
  96. .${classes.smallThumbnail} img {
  97. height: 100%;
  98. width: auto;
  99. }
  100. `);
  101. const title = document.querySelector("#title:has(> h1)");
  102. if (!title) {
  103. return;
  104. }
  105. title.classList.add(classes.title);
  106. const events = new AbortController();
  107. const smallThumbnail = document.createElement("div");
  108. smallThumbnail.classList.add(classes.smallThumbnail);
  109. smallThumbnail.addEventListener(
  110. "click",
  111. async () => {
  112. await largeThumbnail2.expand(watchId);
  113. },
  114. {
  115. signal: events.signal,
  116. },
  117. );
  118. title.insertBefore(smallThumbnail, title.firstChild);
  119. const image = await loadImage(
  120. `https://i.ytimg.com/vi/${encodeURIComponent(watchId)}/default.jpg`,
  121. );
  122. smallThumbnail.append(image);
  123. const dispose = () => {
  124. events.abort();
  125. smallThumbnail.remove();
  126. title.classList.remove(classes.title);
  127. };
  128. document.addEventListener(
  129. "yt-navigate-finish",
  130. () => {
  131. dispose();
  132. },
  133. {
  134. signal: events.signal,
  135. },
  136. );
  137. };
  138. var createLargeThumbnail = () => {
  139. const classes = createCssClasses("hidden", "largeThumbnail");
  140. injectCss(`
  141. .${classes.largeThumbnail} {
  142. display: flex;
  143. align-items: center;
  144. justify-content: center;
  145. position: fixed;
  146. width: 100svw;
  147. height: 100svh;
  148. top: 0;
  149. left: 0;
  150. cursor: zoom-out;
  151. backdrop-filter: brightness(0.5);
  152. z-index: 9999999;
  153. }
  154.  
  155. .${classes.largeThumbnail} img {
  156. max-width: 50%;
  157. max-height: 50%;
  158. width: auto;
  159. height: auto;
  160. vertical-align: middle;
  161. }
  162.  
  163. .${classes.hidden} {
  164. display: none;
  165. }
  166. `);
  167. const largeThumbnail2 = document.createElement("div");
  168. largeThumbnail2.classList.add(classes.hidden, classes.largeThumbnail);
  169. const handleClick = () => {
  170. controller.shrink();
  171. };
  172. const controller = {
  173. expand: async (watchId) => {
  174. const image = await loadImage(
  175. `https://i.ytimg.com/vi/${encodeURIComponent(watchId)}/maxresdefault.jpg`,
  176. );
  177. document.addEventListener("click", handleClick);
  178. largeThumbnail2.replaceChildren(image);
  179. largeThumbnail2.classList.remove(classes.hidden);
  180. },
  181. shrink: () => {
  182. document.removeEventListener("click", handleClick);
  183. largeThumbnail2.classList.add(classes.hidden);
  184. },
  185. };
  186. document.addEventListener("yt-navigate-start", () => {
  187. controller.shrink();
  188. });
  189. document.body.append(largeThumbnail2);
  190. return controller;
  191. };
  192.  
  193. // src/index.ts
  194. var largeThumbnail = createLargeThumbnail();
  195. var apply = async () => {
  196. let watchId;
  197. if (location.pathname.startsWith("/live/")) {
  198. watchId = location.pathname.split("/").pop();
  199. } else {
  200. const searchParams = new URLSearchParams(location.search);
  201. watchId = searchParams.get("v") ?? void 0;
  202. }
  203. if (!watchId) {
  204. return;
  205. }
  206. await applySmallThumbnail(watchId, largeThumbnail);
  207. };
  208. document.addEventListener("yt-page-data-updated", async (event) => {
  209. if (event.detail.pageType !== "watch") {
  210. return;
  211. }
  212. await apply();
  213. });
  214. await apply();
  215. })();

QingJ © 2025

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