layout.ts 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import type { Event } from "@billjs/event-emitter";
  2. import type { FeatureConfig } from "../types";
  3. import { scriptInfo, triesInterval, triesLimit } from "../constants";
  4. import { getFeatures } from "../config";
  5. import { addGlobalStyle, autoPlural, error, getAssetUrl, insertAfter, log, openInNewTab, pauseFor } from "../utils";
  6. import { getEvtData, siteEvents } from "../events";
  7. import { openMenu } from "./menu/menu_old";
  8. import { getGeniusUrl, createLyricsBtn, sanitizeArtists, sanitizeSong, getLyricsCacheEntry } from "./lyrics";
  9. import "./layout.css";
  10. let features: FeatureConfig;
  11. export async function preInitLayout() {
  12. features = await getFeatures();
  13. }
  14. //#MARKER BYTM-Config buttons
  15. /** Adds a watermark beneath the logo */
  16. export function addWatermark() {
  17. const watermark = document.createElement("a");
  18. watermark.role = "button";
  19. watermark.id = "betterytm-watermark";
  20. watermark.className = "style-scope ytmusic-nav-bar";
  21. watermark.innerText = scriptInfo.name;
  22. watermark.title = "Open menu";
  23. watermark.tabIndex = 1000;
  24. watermark.addEventListener("click", () => openMenu());
  25. // when using the tab key to navigate
  26. watermark.addEventListener("keydown", (e) => e.key === "Enter" && openMenu());
  27. const logoElem = document.querySelector("#left-content") as HTMLElement;
  28. insertAfter(logoElem, watermark);
  29. log("Added watermark element:", watermark);
  30. }
  31. /** Called whenever the menu exists to add a BYTM-Configuration button */
  32. export function addConfigMenuOption(container: HTMLElement) {
  33. const cfgElem = document.createElement("div");
  34. cfgElem.innerText = "TODO: BYTM Config";
  35. container.appendChild(cfgElem);
  36. log("Added BYTM-Config button to menu popup");
  37. }
  38. //#MARKER remove upgrade tab
  39. let removeUpgradeTries = 0;
  40. /** Removes the "Upgrade" / YT Music Premium tab from the title / nav bar */
  41. export function removeUpgradeTab() {
  42. const tabElem = document.querySelector("ytmusic-app-layout tp-yt-app-drawer #contentContainer #guide-content #items ytmusic-guide-entry-renderer:nth-child(4)");
  43. const tabElemMini = document.querySelector("ytmusic-app-layout #mini-guide ytmusic-guide-renderer #sections ytmusic-guide-section-renderer #items ytmusic-guide-entry-renderer:nth-child(4)");
  44. if(tabElem || tabElemMini) {
  45. tabElem && tabElem.remove();
  46. tabElemMini && tabElemMini.remove();
  47. log(`Removed upgrade tab after ${removeUpgradeTries} tries`);
  48. }
  49. else if(removeUpgradeTries < triesLimit) {
  50. setTimeout(removeUpgradeTab, triesInterval); // TODO: improve this
  51. removeUpgradeTries++;
  52. }
  53. else
  54. error(`Couldn't find upgrade tab to remove after ${removeUpgradeTries} tries`);
  55. }
  56. //#MARKER volume slider
  57. /** Sets the volume slider to a set size */
  58. export function setVolSliderSize() {
  59. const { volumeSliderSize: size } = features;
  60. if(typeof size !== "number" || isNaN(Number(size)))
  61. return;
  62. const style = `\
  63. .volume-slider.ytmusic-player-bar, .expand-volume-slider.ytmusic-player-bar {
  64. width: ${size}px !important;
  65. }`;
  66. addGlobalStyle(style, "vol-slider");
  67. }
  68. /** Sets the `step` attribute of the volume slider */
  69. export function setVolSliderStep() {
  70. const sliderElem = document.querySelector("tp-yt-paper-slider#volume-slider") as HTMLInputElement;
  71. sliderElem.setAttribute("step", String(features.volumeSliderStep));
  72. }
  73. //#MARKER queue buttons
  74. // TODO: account for the fact initially the elements might not exist, if the site was not opened directly with a video playing or via the /watch path
  75. export function initQueueButtons() {
  76. const addQueueBtns = (evt: Event) => {
  77. let amt = 0;
  78. for(const queueItm of getEvtData<HTMLElement>(evt).childNodes as NodeListOf<HTMLElement>) {
  79. if(!queueItm.classList.contains("bytm-has-queue-btns")) {
  80. addQueueButtons(queueItm);
  81. amt++;
  82. }
  83. }
  84. if(amt > 0)
  85. log(`Added buttons to ${amt} new queue ${autoPlural("item", amt)}`);
  86. };
  87. siteEvents.on("queueChanged", addQueueBtns);
  88. siteEvents.on("autoplayQueueChanged", addQueueBtns);
  89. const queueItems = document.querySelectorAll("#contents.ytmusic-player-queue > ytmusic-player-queue-item");
  90. if(queueItems.length === 0)
  91. return;
  92. queueItems.forEach(itm => addQueueButtons(itm as HTMLElement));
  93. log(`Added buttons to ${queueItems.length} existing queue items`);
  94. }
  95. /**
  96. * Adds the buttons to each item in the current song queue.
  97. * Also observes for changes to add new buttons to new items in the queue.
  98. * @param queueItem The element with tagname `ytmusic-player-queue-item` to add queue buttons to
  99. */
  100. async function addQueueButtons(queueItem: HTMLElement) {
  101. //#SECTION general queue item stuff
  102. const queueBtnsCont = document.createElement("div");
  103. queueBtnsCont.className = "bytm-queue-btn-container";
  104. const songInfo = queueItem.querySelector(".song-info") as HTMLElement;
  105. if(!songInfo)
  106. return false;
  107. const [songEl, artistEl] = (songInfo.querySelectorAll("yt-formatted-string") as NodeListOf<HTMLElement>);
  108. const song = songEl.innerText;
  109. const artist = artistEl.innerText;
  110. if(!song || !artist)
  111. return false;
  112. //#SECTION lyrics btn
  113. const lyricsBtnElem = createLyricsBtn(undefined, false);
  114. {
  115. lyricsBtnElem.title = "Open this song's lyrics in a new tab";
  116. lyricsBtnElem.style.display = "inline-flex";
  117. lyricsBtnElem.style.visibility = "initial";
  118. lyricsBtnElem.style.pointerEvents = "initial";
  119. lyricsBtnElem.addEventListener("click", async () => {
  120. let lyricsUrl: string | undefined;
  121. const artistsSan = sanitizeArtists(artist);
  122. const songSan = sanitizeSong(song);
  123. const cachedLyricsUrl = getLyricsCacheEntry(artistsSan, songSan);
  124. if(cachedLyricsUrl)
  125. lyricsUrl = cachedLyricsUrl;
  126. else if(!songInfo.hasAttribute("data-bytm-loading")) {
  127. const imgEl = lyricsBtnElem.querySelector("img") as HTMLImageElement;
  128. if(!cachedLyricsUrl) {
  129. songInfo.setAttribute("data-bytm-loading", "");
  130. imgEl.classList.add("bytm-spinner");
  131. imgEl.src = getAssetUrl("loading.svg");
  132. }
  133. lyricsUrl = cachedLyricsUrl ?? await getGeniusUrl(artistsSan, songSan);
  134. if(!cachedLyricsUrl) {
  135. songInfo.removeAttribute("data-bytm-loading");
  136. // so the new image doesn't "blink"
  137. setTimeout(() => {
  138. imgEl.src = getAssetUrl("external/genius.png");
  139. imgEl.classList.remove("bytm-spinner");
  140. }, 100);
  141. }
  142. if(!lyricsUrl) {
  143. if(confirm("Couldn't find a lyrics page for this song.\nDo you want to open genius.com to manually search for it?"))
  144. openInNewTab("https://genius.com/search");
  145. return;
  146. }
  147. }
  148. lyricsUrl && openInNewTab(lyricsUrl);
  149. });
  150. }
  151. //#SECTION delete from queue btn
  152. const deleteBtnElem = document.createElement("a");
  153. {
  154. Object.assign(deleteBtnElem, {
  155. title: "Remove this song from the queue",
  156. className: "ytmusic-player-bar bytm-delete-from-queue bytm-generic-btn",
  157. role: "button",
  158. target: "_blank",
  159. rel: "noopener noreferrer",
  160. });
  161. deleteBtnElem.style.visibility = "initial";
  162. deleteBtnElem.addEventListener("click", async () => {
  163. // container of the queue item popup menu - element gets reused for every queue item
  164. let queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown") as HTMLElement;
  165. try {
  166. // three dots button to open the popup menu of a queue item
  167. const dotsBtnElem = queueItem.querySelector("ytmusic-menu-renderer yt-button-shape button") as HTMLButtonElement;
  168. if(queuePopupCont)
  169. queuePopupCont.setAttribute("data-bytm-hidden", "true");
  170. dotsBtnElem.click();
  171. await pauseFor(25);
  172. queuePopupCont = document.querySelector("ytmusic-app ytmusic-popup-container tp-yt-iron-dropdown") as HTMLElement;
  173. if(!queuePopupCont.hasAttribute("data-bytm-hidden"))
  174. queuePopupCont.setAttribute("data-bytm-hidden", "true");
  175. // a little bit janky and unreliable but the only way afaik
  176. const removeFromQueueBtn = queuePopupCont.querySelector("tp-yt-paper-listbox *[role=option]:nth-child(7)") as HTMLElement;
  177. await pauseFor(20);
  178. removeFromQueueBtn.click();
  179. }
  180. catch(err) {
  181. error("Couldn't remove song from queue due to error:", err);
  182. }
  183. finally {
  184. queuePopupCont?.removeAttribute("data-bytm-hidden");
  185. }
  186. });
  187. const imgElem = document.createElement("img");
  188. imgElem.className = "bytm-generic-btn-img";
  189. imgElem.src = getAssetUrl("close.png"); // TODO: make own icon for this
  190. deleteBtnElem.appendChild(imgElem);
  191. }
  192. //#SECTION append elements to DOM
  193. queueBtnsCont.appendChild(lyricsBtnElem);
  194. queueBtnsCont.appendChild(deleteBtnElem);
  195. songInfo.appendChild(queueBtnsCont);
  196. queueItem.classList.add("bytm-has-queue-btns");
  197. return true;
  198. }
  199. //#MARKER better clickable stuff
  200. // TODO: account for the fact initially the elements might not exist, if the site was opened directly with the /watch path
  201. export function addAnchorImprovements() {
  202. void 0;
  203. }