layout.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  1. import { addParent, autoPlural, debounce, fetchAdvanced, pauseFor } from "@sv443-network/userutils";
  2. import { getFeatures } from "../config";
  3. import { siteEvents } from "../siteEvents";
  4. import { addSelectorListener } from "../observers";
  5. import { error, getResourceUrl, log, warn, t, onInteraction, openInTab, getBestThumbnailUrl, getDomain, addStyle, currentMediaType, domLoaded, waitVideoElementReady, getVideoTime, fetchCss, addStyleFromResource } from "../utils";
  6. import { currentParams, scriptInfo } from "../constants";
  7. import { openCfgMenu } from "../menu/menu_old";
  8. import { createCircularBtn } from "../components";
  9. import type { ResourceKey } from "../types";
  10. import "./layout.css";
  11. //#region cfg menu buttons
  12. let logoExchanged = false, improveLogoCalled = false;
  13. /** Adds a watermark beneath the logo */
  14. export async function addWatermark() {
  15. const watermark = document.createElement("a");
  16. watermark.role = "button";
  17. watermark.id = "bytm-watermark";
  18. watermark.classList.add("style-scope", "ytmusic-nav-bar", "bytm-no-select");
  19. watermark.textContent = scriptInfo.name;
  20. watermark.ariaLabel = watermark.title = t("open_menu_tooltip", scriptInfo.name);
  21. watermark.tabIndex = 0;
  22. improveLogo();
  23. const watermarkOpenMenu = (e: MouseEvent | KeyboardEvent) => {
  24. e.stopPropagation();
  25. if((!e.shiftKey && !e.ctrlKey) || logoExchanged)
  26. openCfgMenu();
  27. if(!logoExchanged && (e.shiftKey || e.ctrlKey))
  28. exchangeLogo();
  29. };
  30. onInteraction(watermark, watermarkOpenMenu);
  31. addSelectorListener("navBar", "ytmusic-nav-bar #left-content", {
  32. listener: (logoElem) => logoElem.insertAdjacentElement("afterend", watermark),
  33. });
  34. log("Added watermark element");
  35. }
  36. /** Turns the regular `<img>`-based logo into inline SVG to be able to animate and modify parts of it */
  37. export async function improveLogo() {
  38. try {
  39. if(improveLogoCalled)
  40. return;
  41. improveLogoCalled = true;
  42. const res = await fetchAdvanced("https://music.youtube.com/img/on_platform_logo_dark.svg");
  43. const svg = await res.text();
  44. addSelectorListener("navBar", "ytmusic-logo a", {
  45. listener: (logoElem) => {
  46. logoElem.classList.add("bytm-mod-logo", "bytm-no-select");
  47. logoElem.innerHTML = svg;
  48. logoElem.querySelectorAll("ellipse").forEach((e) => {
  49. e.classList.add("bytm-mod-logo-ellipse");
  50. });
  51. logoElem.querySelector("path")?.classList.add("bytm-mod-logo-path");
  52. log("Swapped logo to inline SVG");
  53. },
  54. });
  55. }
  56. catch(err) {
  57. error("Couldn't improve logo due to an error:", err);
  58. }
  59. }
  60. /** Exchanges the default YTM logo into BetterYTM's logo with a sick ass animation */
  61. function exchangeLogo() {
  62. addSelectorListener("navBar", ".bytm-mod-logo", {
  63. listener: async (logoElem) => {
  64. if(logoElem.classList.contains("bytm-logo-exchanged"))
  65. return;
  66. logoExchanged = true;
  67. logoElem.classList.add("bytm-logo-exchanged");
  68. const iconUrl = await getResourceUrl("img-logo");
  69. const newLogo = document.createElement("img");
  70. newLogo.classList.add("bytm-mod-logo-img");
  71. newLogo.src = iconUrl;
  72. logoElem.insertBefore(newLogo, logoElem.querySelector("svg"));
  73. document.head.querySelectorAll<HTMLLinkElement>("link[rel=\"icon\"]").forEach((e) => {
  74. e.href = iconUrl;
  75. });
  76. setTimeout(() => {
  77. logoElem.querySelectorAll(".bytm-mod-logo-ellipse").forEach(e => e.remove());
  78. }, 1000);
  79. },
  80. });
  81. }
  82. /** Called whenever the avatar popover menu exists on YTM to add a BYTM config menu button to the user menu popover */
  83. export async function addConfigMenuOptionYTM(container: HTMLElement) {
  84. const cfgOptElem = document.createElement("div");
  85. cfgOptElem.classList.add("bytm-cfg-menu-option");
  86. const cfgOptItemElem = document.createElement("div");
  87. cfgOptItemElem.classList.add("bytm-cfg-menu-option-item");
  88. cfgOptItemElem.role = "button";
  89. cfgOptItemElem.tabIndex = 0;
  90. cfgOptItemElem.ariaLabel = cfgOptItemElem.title = t("open_menu_tooltip", scriptInfo.name);
  91. onInteraction(cfgOptItemElem, async (e: MouseEvent | KeyboardEvent) => {
  92. const settingsBtnElem = document.querySelector<HTMLElement>("ytmusic-nav-bar ytmusic-settings-button tp-yt-paper-icon-button");
  93. settingsBtnElem?.click();
  94. await pauseFor(20);
  95. if((!e.shiftKey && !e.ctrlKey) || logoExchanged)
  96. openCfgMenu();
  97. if(!logoExchanged && (e.shiftKey || e.ctrlKey))
  98. exchangeLogo();
  99. });
  100. const cfgOptIconElem = document.createElement("img");
  101. cfgOptIconElem.classList.add("bytm-cfg-menu-option-icon");
  102. cfgOptIconElem.src = await getResourceUrl("img-logo");
  103. const cfgOptTextElem = document.createElement("div");
  104. cfgOptTextElem.classList.add("bytm-cfg-menu-option-text");
  105. cfgOptTextElem.textContent = t("config_menu_option", scriptInfo.name);
  106. cfgOptItemElem.appendChild(cfgOptIconElem);
  107. cfgOptItemElem.appendChild(cfgOptTextElem);
  108. cfgOptElem.appendChild(cfgOptItemElem);
  109. container.appendChild(cfgOptElem);
  110. improveLogo();
  111. log("Added BYTM-Configuration button to menu popover");
  112. }
  113. /** Called whenever the titlebar (masthead) exists on YT to add a BYTM config menu button */
  114. export async function addConfigMenuOptionYT(container: HTMLElement) {
  115. // TODO:
  116. const cfgOptElem = document.createElement("div");
  117. cfgOptElem.classList.add("bytm-yt-cfg-menu-option", "darkreader-ignore");
  118. cfgOptElem.role = "button";
  119. cfgOptElem.tabIndex = 0;
  120. cfgOptElem.ariaLabel = cfgOptElem.title = t("open_menu_tooltip", scriptInfo.name);
  121. const cfgOptImgElem = document.createElement("img");
  122. cfgOptImgElem.classList.add("bytm-yt-cfg-menu-option-icon");
  123. cfgOptImgElem.src = await getResourceUrl("img-logo");
  124. const cfgOptItemElem = document.createElement("div");
  125. cfgOptItemElem.classList.add("bytm-yt-cfg-menu-option-item");
  126. cfgOptItemElem.textContent = scriptInfo.name;
  127. cfgOptElem.appendChild(cfgOptImgElem);
  128. cfgOptElem.appendChild(cfgOptItemElem);
  129. onInteraction(cfgOptElem, openCfgMenu);
  130. const firstChild = container?.firstElementChild;
  131. if(firstChild)
  132. container.insertBefore(cfgOptElem, firstChild);
  133. else
  134. return error("Couldn't add config menu option to YT titlebar - couldn't find container element");
  135. }
  136. //#region rem upgrade tab
  137. /** Removes the "Upgrade" / YT Music Premium tab from the sidebar */
  138. export async function removeUpgradeTab() {
  139. addSelectorListener("sideBar", "#contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
  140. listener: (tabElemLarge) => {
  141. tabElemLarge.remove();
  142. log("Removed large upgrade tab");
  143. },
  144. });
  145. addSelectorListener("sideBarMini", "ytmusic-guide-renderer #sections ytmusic-guide-section-renderer[is-primary] #items ytmusic-guide-entry-renderer:nth-of-type(4)", {
  146. listener: (tabElemSmall) => {
  147. tabElemSmall.remove();
  148. log("Removed small upgrade tab");
  149. },
  150. });
  151. }
  152. //#region anchor improvements
  153. /** Adds anchors around elements and tweaks existing ones so songs are easier to open in a new tab */
  154. export async function addAnchorImprovements() {
  155. try {
  156. const css = await fetchCss("css-anchor_improvements");
  157. if(css)
  158. addStyle(css, "anchor-improvements");
  159. }
  160. catch(err) {
  161. error("Couldn't add anchor improvements CSS due to an error:", err);
  162. }
  163. //#region carousel shelves
  164. try {
  165. const preventDefault = (e: MouseEvent) => e.preventDefault();
  166. /** Adds anchor improvements to &lt;ytmusic-responsive-list-item-renderer&gt; */
  167. const addListItemAnchors = (items: NodeListOf<HTMLElement>) => {
  168. for(const item of items) {
  169. if(item.classList.contains("bytm-anchor-improved"))
  170. continue;
  171. item.classList.add("bytm-anchor-improved");
  172. const thumbnailElem = item.querySelector<HTMLElement>(".left-items");
  173. const titleElem = item.querySelector<HTMLAnchorElement>(".title-column .title a");
  174. if(!thumbnailElem || !titleElem)
  175. continue;
  176. const anchorElem = document.createElement("a");
  177. anchorElem.classList.add("bytm-anchor", "bytm-carousel-shelf-anchor");
  178. anchorElem.href = titleElem?.href ?? "#";
  179. anchorElem.target = "_self";
  180. anchorElem.role = "button";
  181. anchorElem.addEventListener("click", preventDefault);
  182. addParent(thumbnailElem, anchorElem);
  183. }
  184. };
  185. // home page
  186. addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-carousel-shelf-renderer ytmusic-responsive-list-item-renderer", {
  187. continuous: true,
  188. all: true,
  189. listener: addListItemAnchors,
  190. });
  191. // related tab in /watch
  192. addSelectorListener("body", "ytmusic-tab-renderer[page-type=\"MUSIC_PAGE_TYPE_TRACK_RELATED\"] ytmusic-responsive-list-item-renderer", {
  193. continuous: true,
  194. all: true,
  195. listener: addListItemAnchors,
  196. });
  197. // playlists
  198. addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer", {
  199. continuous: true,
  200. all: true,
  201. listener: addListItemAnchors,
  202. });
  203. // generic shelves
  204. addSelectorListener("body", "#contents.ytmusic-section-list-renderer ytmusic-shelf-renderer ytmusic-responsive-list-item-renderer", {
  205. continuous: true,
  206. all: true,
  207. listener: addListItemAnchors,
  208. });
  209. }
  210. catch(err) {
  211. error("Couldn't improve carousel shelf anchors due to an error:", err);
  212. }
  213. //#region sidebar
  214. try {
  215. const addSidebarAnchors = (sidebarCont: HTMLElement) => {
  216. const items = sidebarCont.parentNode!.querySelectorAll<HTMLElement>("ytmusic-guide-entry-renderer tp-yt-paper-item");
  217. improveSidebarAnchors(items);
  218. return items.length;
  219. };
  220. addSelectorListener("sideBar", "#contentContainer #guide-content #items ytmusic-guide-entry-renderer", {
  221. listener: (sidebarCont) => {
  222. const itemsAmt = addSidebarAnchors(sidebarCont);
  223. log(`Added anchors around ${itemsAmt} sidebar ${autoPlural("item", itemsAmt)}`);
  224. },
  225. });
  226. addSelectorListener("sideBarMini", "ytmusic-guide-renderer ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer", {
  227. listener: (miniSidebarCont) => {
  228. const itemsAmt = addSidebarAnchors(miniSidebarCont);
  229. log(`Added anchors around ${itemsAmt} mini sidebar ${autoPlural("item", itemsAmt)}`);
  230. },
  231. });
  232. }
  233. catch(err) {
  234. error("Couldn't add anchors to sidebar items due to an error:", err);
  235. }
  236. }
  237. const sidebarPaths = [
  238. "/",
  239. "/explore",
  240. "/library",
  241. ];
  242. /**
  243. * Adds anchors to the sidebar items so they can be opened in a new tab
  244. * @param sidebarItem
  245. */
  246. function improveSidebarAnchors(sidebarItems: NodeListOf<HTMLElement>) {
  247. sidebarItems.forEach((item, i) => {
  248. const anchorElem = document.createElement("a");
  249. anchorElem.classList.add("bytm-anchor", "bytm-no-select");
  250. anchorElem.role = "button";
  251. anchorElem.target = "_self";
  252. anchorElem.href = sidebarPaths[i] ?? "#";
  253. anchorElem.ariaLabel = anchorElem.title = t("middle_click_open_tab");
  254. anchorElem.addEventListener("click", (e) => {
  255. e.preventDefault();
  256. });
  257. addParent(item, anchorElem);
  258. });
  259. }
  260. //#region rem tracking param
  261. /** Removes the ?si tracking parameter from share URLs */
  262. export async function initRemShareTrackParam() {
  263. const removeSiParam = (inputElem: HTMLInputElement) => {
  264. try {
  265. if(!inputElem.value.match(/(&|\?)si=/i))
  266. return;
  267. const url = new URL(inputElem.value);
  268. url.searchParams.delete("si");
  269. inputElem.value = String(url);
  270. log(`Removed tracking parameter from share link -> ${url}`);
  271. }
  272. catch(err) {
  273. warn("Couldn't remove tracking parameter from share link due to error:", err);
  274. }
  275. };
  276. const [sharePanelSel, inputSel] = (() => {
  277. switch(getDomain()) {
  278. case "ytm": return ["tp-yt-paper-dialog ytmusic-unified-share-panel-renderer", "input#share-url"];
  279. case "yt": return ["ytd-unified-share-panel-renderer", "input#share-url"];
  280. }
  281. })();
  282. addSelectorListener("body", sharePanelSel, {
  283. listener: (sharePanelEl) => {
  284. const obs = new MutationObserver(() => {
  285. const inputElem = sharePanelEl.querySelector<HTMLInputElement>(inputSel);
  286. inputElem && removeSiParam(inputElem);
  287. });
  288. obs.observe(sharePanelEl, {
  289. childList: true,
  290. subtree: true,
  291. attributeFilter: ["aria-hidden", "aria-checked", "checked"],
  292. });
  293. },
  294. });
  295. }
  296. //#region fix spacing
  297. /** Applies global CSS to fix various spacings */
  298. export async function fixSpacing() {
  299. if(!await addStyleFromResource("css-fix_spacing"))
  300. error("Couldn't fix spacing");
  301. }
  302. //#region above queue btns
  303. export async function initAboveQueueBtns() {
  304. const { scrollToActiveSongBtn, clearQueueBtn } = getFeatures();
  305. if(!scrollToActiveSongBtn && !clearQueueBtn)
  306. return;
  307. addSelectorListener("sidePanel", "ytmusic-tab-renderer ytmusic-queue-header-renderer #buttons", {
  308. async listener(rightBtnsEl) {
  309. const aboveQueueBtnCont = document.createElement("div");
  310. aboveQueueBtnCont.id = "bytm-above-queue-btn-cont";
  311. addParent(rightBtnsEl, aboveQueueBtnCont);
  312. const headerEl = rightBtnsEl.closest<HTMLElement>("ytmusic-queue-header-renderer");
  313. if(!headerEl)
  314. return error("Couldn't find queue header element while adding above queue buttons");
  315. siteEvents.on("fullscreenToggled", (isFullscreen) => {
  316. headerEl.classList[isFullscreen ? "add" : "remove"]("hidden");
  317. });
  318. const contBtns = [
  319. {
  320. condition: scrollToActiveSongBtn,
  321. id: "scroll-to-active",
  322. resourceName: "icon-skip_to",
  323. titleKey: "scroll_to_playing",
  324. async interaction() {
  325. const activeItem = document.querySelector<HTMLElement>("#side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"loading\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"playing\"], #side-panel .ytmusic-player-queue ytmusic-player-queue-item[play-button-state=\"paused\"]");
  326. if(!activeItem)
  327. return;
  328. activeItem.scrollIntoView({
  329. behavior: "smooth",
  330. block: "center",
  331. inline: "center",
  332. });
  333. },
  334. },
  335. {
  336. condition: clearQueueBtn,
  337. id: "clear-queue",
  338. resourceName: "icon-clear_list",
  339. titleKey: "clear_list",
  340. async interaction() {
  341. try {
  342. // TODO: better confirmation dialog?
  343. if(!confirm(t("clear_list_confirm")))
  344. return;
  345. const url = new URL(location.href);
  346. url.searchParams.delete("list");
  347. url.searchParams.set("t", String(await getVideoTime(0)));
  348. location.assign(url);
  349. }
  350. catch(err) {
  351. error("Couldn't clear queue due to an error:", err);
  352. }
  353. },
  354. },
  355. ];
  356. if(contBtns.some(b => Boolean(b.condition))) {
  357. if(!await addStyleFromResource("css-above_queue_btns"))
  358. error("Couldn't add CSS for above queue buttons");
  359. const wrapperElem = document.createElement("div");
  360. wrapperElem.id = "bytm-above-queue-btn-wrapper";
  361. for(const item of contBtns) {
  362. if(Boolean(item.condition) === false)
  363. continue;
  364. const btnElem = await createCircularBtn({
  365. resourceName: item.resourceName as ResourceKey,
  366. onClick: item.interaction,
  367. title: t(item.titleKey),
  368. });
  369. btnElem.id = `bytm-${item.id}-btn`;
  370. btnElem.classList.add("ytmusic-player-bar", "bytm-generic-btn", "bytm-above-queue-btn");
  371. wrapperElem.appendChild(btnElem);
  372. }
  373. rightBtnsEl.insertAdjacentElement("beforebegin", wrapperElem);
  374. }
  375. },
  376. });
  377. }
  378. //#region thumbnail overlay
  379. /** To be changed when the toggle button is pressed - used to invert the state of "showOverlay" */
  380. let invertOverlay = false;
  381. export async function initThumbnailOverlay() {
  382. const toggleBtnShown = getFeatures().thumbnailOverlayToggleBtnShown;
  383. if(getFeatures().thumbnailOverlayBehavior === "never" && !toggleBtnShown)
  384. return;
  385. // so the script doesn't wait until a /watch page is loaded
  386. waitVideoElementReady().then(() => {
  387. const playerSelector = "ytmusic-player#player";
  388. const playerEl = document.querySelector<HTMLElement>(playerSelector);
  389. if(!playerEl)
  390. return error("Couldn't find video player element while adding thumbnail overlay");
  391. /** Checks and updates the overlay and toggle button states based on the current song type (yt video or ytm song) */
  392. const updateOverlayVisibility = async () => {
  393. if(!domLoaded)
  394. return;
  395. const behavior = getFeatures().thumbnailOverlayBehavior;
  396. let showOverlay = behavior === "always";
  397. const isVideo = currentMediaType() === "video";
  398. if(behavior === "videosOnly" && isVideo)
  399. showOverlay = true;
  400. else if(behavior === "songsOnly" && !isVideo)
  401. showOverlay = true;
  402. showOverlay = invertOverlay ? !showOverlay : showOverlay;
  403. const overlayElem = document.querySelector<HTMLElement>("#bytm-thumbnail-overlay");
  404. const thumbElem = document.querySelector<HTMLElement>("#bytm-thumbnail-overlay-img");
  405. const indicatorElem = document.querySelector<HTMLElement>("#bytm-thumbnail-overlay-indicator");
  406. if(overlayElem)
  407. overlayElem.style.display = showOverlay ? "block" : "none";
  408. if(thumbElem)
  409. thumbElem.ariaHidden = String(!showOverlay);
  410. if(indicatorElem) {
  411. indicatorElem.style.display = showOverlay ? "block" : "none";
  412. indicatorElem.ariaHidden = String(!showOverlay);
  413. }
  414. if(getFeatures().thumbnailOverlayToggleBtnShown) {
  415. addSelectorListener("playerBarMiddleButtons", "#bytm-thumbnail-overlay-toggle", {
  416. async listener(toggleBtnElem) {
  417. const toggleBtnImgElem = toggleBtnElem.querySelector<HTMLImageElement>("img");
  418. if(toggleBtnImgElem)
  419. toggleBtnImgElem.src = await getResourceUrl(`icon-image${showOverlay ? "_filled" : ""}` as "icon-image" | "icon-image_filled");
  420. if(toggleBtnElem)
  421. toggleBtnElem.ariaLabel = toggleBtnElem.title = t(`thumbnail_overlay_toggle_btn_tooltip${showOverlay ? "_hide" : "_show"}`);
  422. },
  423. });
  424. }
  425. };
  426. const applyThumbUrl = async (watchId: string) => {
  427. const thumbUrl = await getBestThumbnailUrl(watchId);
  428. if(thumbUrl) {
  429. const toggleBtnElem = document.querySelector<HTMLAnchorElement>("#bytm-thumbnail-overlay-toggle");
  430. const thumbImgElem = document.querySelector<HTMLImageElement>("#bytm-thumbnail-overlay-img");
  431. if(toggleBtnElem)
  432. toggleBtnElem.href = thumbUrl;
  433. if(thumbImgElem)
  434. thumbImgElem.src = thumbUrl;
  435. }
  436. else error("Couldn't get thumbnail URL for watch ID", watchId);
  437. };
  438. const unsubWatchIdChanged = siteEvents.on("watchIdChanged", (watchId) => {
  439. unsubWatchIdChanged();
  440. addSelectorListener("body", "#bytm-thumbnail-overlay", {
  441. listener: () => {
  442. applyThumbUrl(watchId);
  443. updateOverlayVisibility();
  444. },
  445. });
  446. });
  447. const createElements = async () => {
  448. // overlay
  449. const overlayElem = document.createElement("div");
  450. overlayElem.id = "bytm-thumbnail-overlay";
  451. overlayElem.title = ""; // prevent child titles from propagating
  452. overlayElem.classList.add("bytm-no-select");
  453. overlayElem.style.display = "none";
  454. let indicatorElem: HTMLImageElement | undefined;
  455. if(getFeatures().thumbnailOverlayShowIndicator) {
  456. indicatorElem = document.createElement("img");
  457. indicatorElem.id = "bytm-thumbnail-overlay-indicator";
  458. indicatorElem.src = await getResourceUrl("icon-image");
  459. indicatorElem.role = "presentation";
  460. indicatorElem.title = indicatorElem.ariaLabel = t("thumbnail_overlay_indicator_tooltip");
  461. indicatorElem.ariaHidden = "true";
  462. indicatorElem.style.display = "none";
  463. indicatorElem.style.opacity = String(getFeatures().thumbnailOverlayIndicatorOpacity / 100);
  464. }
  465. const thumbImgElem = document.createElement("img");
  466. thumbImgElem.id = "bytm-thumbnail-overlay-img";
  467. thumbImgElem.role = "presentation";
  468. thumbImgElem.ariaHidden = "true";
  469. thumbImgElem.style.objectFit = getFeatures().thumbnailOverlayImageFit;
  470. overlayElem.appendChild(thumbImgElem);
  471. playerEl.appendChild(overlayElem);
  472. indicatorElem && playerEl.appendChild(indicatorElem);
  473. siteEvents.on("watchIdChanged", async (watchId) => {
  474. invertOverlay = false;
  475. applyThumbUrl(watchId);
  476. updateOverlayVisibility();
  477. });
  478. if(currentParams.has("v")) {
  479. applyThumbUrl(currentParams.get("v")!);
  480. updateOverlayVisibility();
  481. }
  482. // toggle button
  483. if(toggleBtnShown) {
  484. const toggleBtnElem = document.createElement("a");
  485. toggleBtnElem.id = "bytm-thumbnail-overlay-toggle";
  486. toggleBtnElem.role = "button";
  487. toggleBtnElem.tabIndex = 0;
  488. toggleBtnElem.classList.add("ytmusic-player-bar", "bytm-generic-btn", "bytm-no-select");
  489. onInteraction(toggleBtnElem, (e) => {
  490. if(e.shiftKey)
  491. return openInTab(toggleBtnElem.href, e instanceof MouseEvent);
  492. invertOverlay = !invertOverlay;
  493. updateOverlayVisibility();
  494. });
  495. const imgElem = document.createElement("img");
  496. imgElem.classList.add("bytm-generic-btn-img");
  497. toggleBtnElem.appendChild(imgElem);
  498. addSelectorListener("playerBarMiddleButtons", "ytmusic-like-button-renderer#like-button-renderer", {
  499. listener: (likeContainer) => likeContainer.insertAdjacentElement("afterend", toggleBtnElem),
  500. });
  501. }
  502. log("Added thumbnail overlay");
  503. };
  504. addSelectorListener("mainPanel", playerSelector, {
  505. listener(playerEl) {
  506. if(playerEl.getAttribute("player-ui-state") === "INACTIVE") {
  507. const obs = new MutationObserver(() => {
  508. if(playerEl.getAttribute("player-ui-state") === "INACTIVE")
  509. return;
  510. createElements();
  511. obs.disconnect();
  512. });
  513. obs.observe(playerEl, {
  514. attributes: true,
  515. attributeFilter: ["player-ui-state"],
  516. });
  517. }
  518. else
  519. createElements();
  520. },
  521. });
  522. });
  523. }
  524. //#region hide cursor on idle
  525. export async function initHideCursorOnIdle() {
  526. addSelectorListener("mainPanel", "ytmusic-player#player", {
  527. listener(vidContainer) {
  528. const overlaySelector = "ytmusic-player #song-media-window";
  529. const overlayElem = document.querySelector<HTMLElement>(overlaySelector);
  530. if(!overlayElem)
  531. return warn("Couldn't find overlay element while initializing cursor hiding");
  532. /** Timer after which the cursor is hidden */
  533. let cursorHideTimer: ReturnType<typeof setTimeout>;
  534. /** Timer for the opacity transition while switching to the hidden state */
  535. let hideTransTimer: ReturnType<typeof setTimeout> | undefined;
  536. const hide = () => {
  537. if(!getFeatures().hideCursorOnIdle)
  538. return;
  539. if(vidContainer.classList.contains("bytm-cursor-hidden"))
  540. return;
  541. overlayElem.style.opacity = ".000001 !important";
  542. hideTransTimer = setTimeout(() => {
  543. overlayElem.style.display = "none";
  544. vidContainer.style.cursor = "none";
  545. vidContainer.classList.add("bytm-cursor-hidden");
  546. hideTransTimer = undefined;
  547. }, 200);
  548. };
  549. const show = () => {
  550. hideTransTimer && clearTimeout(hideTransTimer);
  551. if(!vidContainer.classList.contains("bytm-cursor-hidden"))
  552. return;
  553. vidContainer.classList.remove("bytm-cursor-hidden");
  554. vidContainer.style.cursor = "initial";
  555. overlayElem.style.display = "initial";
  556. overlayElem.style.opacity = "1 !important";
  557. };
  558. const cursorHideTimerCb = () =>
  559. cursorHideTimer = setTimeout(hide, getFeatures().hideCursorOnIdleDelay * 1000);
  560. const onMove = () => {
  561. cursorHideTimer && clearTimeout(cursorHideTimer);
  562. show();
  563. cursorHideTimerCb();
  564. };
  565. vidContainer.addEventListener("mouseenter", onMove);
  566. vidContainer.addEventListener("mousemove", debounce(onMove, 200, "rising"));
  567. vidContainer.addEventListener("mouseleave", () => {
  568. cursorHideTimer && clearTimeout(cursorHideTimer);
  569. hideTransTimer && clearTimeout(hideTransTimer);
  570. hide();
  571. });
  572. vidContainer.addEventListener("click", () => {
  573. show();
  574. cursorHideTimerCb();
  575. setTimeout(hide, 3000);
  576. });
  577. log("Initialized cursor hiding on idle");
  578. },
  579. });
  580. }
  581. //#region fix HDR
  582. /** Prevents visual issues when using HDR */
  583. export async function fixHdrIssues() {
  584. if(!await addStyleFromResource("css-fix_hdr"))
  585. error("Couldn't load stylesheet to fix HDR issues");
  586. else
  587. log("Fixed HDR issues");
  588. }