layout.ts 22 KB

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