layout.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. import { dbg, info, triesInterval, triesLimit } from "../BetterYTM.user";
  2. import { defaultFeatures, getFeatures, saveFeatureConf } from "../config";
  3. import { addGlobalStyle, insertAfter } from "../utils";
  4. import { featInfo } from "./index";
  5. import type { FeatureConfig } from "../types";
  6. let features: FeatureConfig;
  7. export async function initLayout() {
  8. features = await getFeatures();
  9. }
  10. //#MARKER menu
  11. const branch = dbg ? "develop" : "main";
  12. /** Adds an element to open the BetterYTM menu */
  13. export async function addMenu() {
  14. // bg & menu
  15. const backgroundElem = document.createElement("div");
  16. backgroundElem.id = "betterytm-menu-bg";
  17. backgroundElem.title = "Click here to close the menu";
  18. backgroundElem.style.visibility = "hidden";
  19. backgroundElem.style.display = "none";
  20. backgroundElem.addEventListener("click", (e) => {
  21. if((e.target as HTMLElement).id === "betterytm-menu-bg")
  22. closeMenu();
  23. });
  24. const menuContainer = document.createElement("div");
  25. menuContainer.title = "";
  26. menuContainer.id = "betterytm-menu";
  27. menuContainer.style.borderRadius = "15px";
  28. menuContainer.style.display = "flex";
  29. menuContainer.style.flexDirection = "column";
  30. menuContainer.style.justifyContent = "space-between";
  31. // title
  32. const titleCont = document.createElement("div");
  33. titleCont.style.padding = "8px 20px 15px 8px";
  34. titleCont.style.display = "flex";
  35. titleCont.style.justifyContent = "space-between";
  36. titleCont.id = "betterytm-menu-titlecont";
  37. const titleElem = document.createElement("h2");
  38. titleElem.id = "betterytm-menu-title";
  39. titleElem.innerText = "BetterYTM - Configuration";
  40. const linksCont = document.createElement("div");
  41. linksCont.id = "betterytm-menu-linkscont";
  42. const addLink = (imgSrc: string, href: string, title: string) => {
  43. const anchorElem = document.createElement("a");
  44. anchorElem.className = "betterytm-menu-link";
  45. anchorElem.rel = "noopener noreferrer";
  46. anchorElem.target = "_blank";
  47. anchorElem.href = href;
  48. anchorElem.title = title;
  49. anchorElem.style.marginLeft = "10px";
  50. const imgElem = document.createElement("img");
  51. imgElem.className = "betterytm-menu-img";
  52. imgElem.src = imgSrc;
  53. imgElem.style.width = "32px";
  54. imgElem.style.height = "32px";
  55. anchorElem.appendChild(imgElem);
  56. linksCont.appendChild(anchorElem);
  57. };
  58. addLink(`https://raw.githubusercontent.com/Sv443/BetterYTM/${branch}/resources/external/github.png`, info.namespace, `${info.name} on GitHub`);
  59. addLink(`https://raw.githubusercontent.com/Sv443/BetterYTM/${branch}/resources/external/greasyfork.png`, "https://greasyfork.org/xyz", `${info.name} on GreasyFork`);
  60. const closeElem = document.createElement("img");
  61. closeElem.id = "betterytm-menu-close";
  62. closeElem.src = `https://raw.githubusercontent.com/Sv443/BetterYTM/${branch}/resources/icon/close.png`;
  63. closeElem.title = "Click to close the menu";
  64. closeElem.style.marginLeft = "50px";
  65. closeElem.style.width = "32px";
  66. closeElem.style.height = "32px";
  67. closeElem.addEventListener("click", closeMenu);
  68. linksCont.appendChild(closeElem);
  69. titleCont.appendChild(titleElem);
  70. titleCont.appendChild(linksCont);
  71. // TODO: features
  72. const featuresCont = document.createElement("div");
  73. featuresCont.id = "betterytm-menu-opts";
  74. featuresCont.style.display = "flex";
  75. featuresCont.style.flexDirection = "column";
  76. /** Gets called whenever the feature config is changed */
  77. const confChanged = async (key: keyof typeof defaultFeatures, initialVal: number | boolean, newVal: number | boolean) => {
  78. dbg && console.info(`BetterYTM: Feature config changed, key '${key}' from value '${initialVal}' to '${newVal}'`);
  79. const featConf = { ...await getFeatures() };
  80. featConf[key] = newVal as never;
  81. await saveFeatureConf(featConf);
  82. dbg && console.log("BetterYTM: Saved feature config changes:\n", await GM.getValue("betterytm-config"));
  83. };
  84. const features = await getFeatures();
  85. const featKeys = Object.keys(features);
  86. for(const key of featKeys) {
  87. const ftInfo = featInfo[key as keyof typeof features];
  88. if(!ftInfo)
  89. continue;
  90. const { desc, type, default: ftDef } = ftInfo;
  91. // @ts-ignore
  92. const step = ftInfo?.step ?? undefined;
  93. const val = features[key as keyof typeof features];
  94. const initialVal = val || ftDef || undefined;
  95. const ftConfElem = document.createElement("div");
  96. ftConfElem.id = `betterytm-ftconf-${key}`;
  97. ftConfElem.style.display = "flex";
  98. ftConfElem.style.flexDirection = "row";
  99. ftConfElem.style.justifyContent = "space-between";
  100. ftConfElem.style.padding = "8px 20px";
  101. {
  102. const textElem = document.createElement("span");
  103. textElem.style.display = "inline-block";
  104. textElem.style.fontSize = "15px";
  105. textElem.innerText = desc;
  106. ftConfElem.appendChild(textElem);
  107. }
  108. {
  109. let inputType = "text";
  110. switch(type)
  111. {
  112. case "toggle":
  113. inputType = "checkbox";
  114. break;
  115. case "slider":
  116. inputType = "range";
  117. break;
  118. case "number":
  119. inputType = "number";
  120. break;
  121. }
  122. const inputElemId = `betterytm-ftconf-${key}-input`;
  123. const ctrlElem = document.createElement("span");
  124. ctrlElem.style.display = "inline-block";
  125. ctrlElem.style.whiteSpace = "nowrap";
  126. const inputElem = document.createElement("input");
  127. inputElem.id = inputElemId;
  128. inputElem.style.marginRight = "37px";
  129. inputElem.type = inputType;
  130. if(type === "toggle")
  131. inputElem.style.marginLeft = "5px";
  132. if(typeof initialVal !== "undefined")
  133. inputElem.value = String(initialVal);
  134. if(type === "number" && step)
  135. inputElem.step = step;
  136. // @ts-ignore
  137. if(ftInfo.min && ftInfo.max) {
  138. // @ts-ignore
  139. inputElem.min = ftInfo.min;
  140. // @ts-ignore
  141. inputElem.max = ftInfo.max;
  142. }
  143. if(type === "toggle" && typeof initialVal !== "undefined")
  144. inputElem.checked = Boolean(initialVal);
  145. const fmtVal = (v: unknown) => String(v);
  146. const toggleLabelText = (toggled: boolean) => toggled ? "On" : "Off";
  147. let labelElem: HTMLLabelElement | undefined;
  148. if(type === "slider") {
  149. labelElem = document.createElement("label");
  150. labelElem.classList.add("betterytm-ftconf-label");
  151. labelElem.style.marginRight = "20px";
  152. labelElem.style.fontSize = "16px";
  153. labelElem.htmlFor = inputElemId;
  154. labelElem.innerText = fmtVal(initialVal);
  155. inputElem.addEventListener("input", () => {
  156. if(labelElem)
  157. labelElem.innerText = fmtVal(parseInt(inputElem.value));
  158. });
  159. }
  160. else if(type === "toggle" && typeof initialVal !== "undefined") {
  161. labelElem = document.createElement("label");
  162. labelElem.classList.add("betterytm-ftconf-label");
  163. labelElem.style.paddingLeft = "10px";
  164. labelElem.style.paddingRight = "5px";
  165. labelElem.style.fontSize = "16px";
  166. labelElem.htmlFor = inputElemId;
  167. labelElem.innerText = toggleLabelText(Boolean(initialVal));
  168. inputElem.addEventListener("input", () => {
  169. if(labelElem)
  170. labelElem.innerText = toggleLabelText(inputElem.checked);
  171. });
  172. }
  173. inputElem.addEventListener("input", ({ currentTarget }) => {
  174. const elem = currentTarget as HTMLInputElement;
  175. let v = parseInt(elem.value);
  176. if(isNaN(v))
  177. v = Number(elem.value);
  178. if(typeof initialVal !== "undefined")
  179. confChanged(key as keyof FeatureConfig, initialVal, (type !== "toggle" ? v : elem.checked));
  180. });
  181. const resetElem = document.createElement("button");
  182. resetElem.innerText = "Reset";
  183. resetElem.addEventListener("click", () => {
  184. inputElem[type !== "toggle" ? "value" : "checked"] = ftDef as never;
  185. if(labelElem) {
  186. if(type === "toggle")
  187. labelElem.innerText = toggleLabelText(inputElem.checked);
  188. else
  189. labelElem.innerText = fmtVal(parseInt(inputElem.value));
  190. }
  191. if(typeof initialVal !== "undefined")
  192. confChanged(key as keyof FeatureConfig, initialVal, ftDef);
  193. });
  194. labelElem && ctrlElem.appendChild(labelElem);
  195. ctrlElem.appendChild(inputElem);
  196. ctrlElem.appendChild(resetElem);
  197. ftConfElem.appendChild(ctrlElem);
  198. }
  199. featuresCont.appendChild(ftConfElem);
  200. }
  201. const footerElem = document.createElement("div");
  202. footerElem.style.marginTop = "20px";
  203. footerElem.style.fontSize = "17px";
  204. footerElem.style.textDecoration = "underline";
  205. footerElem.style.padding = "8px 20px";
  206. footerElem.innerText = "You need to reload the page to apply changes.";
  207. const reloadElem = document.createElement("button");
  208. reloadElem.style.marginLeft = "20px";
  209. reloadElem.innerText = "Reload now";
  210. reloadElem.title = "Click to reload the page";
  211. reloadElem.addEventListener("click", () => location.reload());
  212. footerElem.appendChild(reloadElem);
  213. featuresCont.appendChild(footerElem);
  214. // finalize
  215. const menuBody = document.createElement("div");
  216. menuBody.id = "betterytm-menu-body";
  217. menuBody.appendChild(titleCont);
  218. menuBody.appendChild(featuresCont);
  219. const versionCont = document.createElement("div");
  220. versionCont.style.display = "flex";
  221. versionCont.style.justifyContent = "space-around";
  222. versionCont.style.fontSize = "1.15em";
  223. versionCont.style.marginTop = "10px";
  224. versionCont.style.marginBottom = "5px";
  225. const versionElem = document.createElement("span");
  226. versionElem.id = "betterytm-menu-version";
  227. versionElem.innerText = `v${info.version}`;
  228. versionCont.appendChild(versionElem);
  229. featuresCont.appendChild(versionCont);
  230. menuContainer.appendChild(menuBody);
  231. menuContainer.appendChild(versionCont);
  232. backgroundElem.appendChild(menuContainer);
  233. document.body.appendChild(backgroundElem);
  234. // add style
  235. const menuStyle = `\
  236. #betterytm-menu-bg {
  237. display: block;
  238. position: fixed;
  239. width: 100vw;
  240. height: 100vh;
  241. top: 0;
  242. left: 0;
  243. z-index: 15;
  244. background-color: rgba(0, 0, 0, 0.6);
  245. }
  246. #betterytm-menu {
  247. display: inline-block;
  248. position: fixed;
  249. width: 50vw;
  250. height: auto;
  251. min-height: 500px;
  252. left: 25vw;
  253. top: 25vh;
  254. z-index: 16;
  255. overflow: auto;
  256. padding: 8px;
  257. color: #fff;
  258. background-color: #212121;
  259. }
  260. #betterytm-menu-titlecont {
  261. display: flex;
  262. }
  263. #betterytm-menu-title {
  264. font-size: 20px;
  265. margin-top: 5px;
  266. margin-bottom: 8px;
  267. }
  268. #betterytm-menu-linkscont {
  269. display: flex;
  270. }
  271. .betterytm-menu-link {
  272. display: inline-block;
  273. }
  274. /*.betterytm-menu-img {
  275. }*/
  276. #betterytm-menu-close {
  277. cursor: pointer;
  278. }
  279. .betterytm-ftconf-label {
  280. user-select: none;
  281. }`;
  282. dbg && console.log("BetterYTM: Added menu elem:", backgroundElem);
  283. addGlobalStyle(menuStyle, "menu");
  284. }
  285. export function closeMenu() {
  286. const menuBg = document.querySelector("#betterytm-menu-bg") as HTMLElement;
  287. menuBg.style.visibility = "hidden";
  288. menuBg.style.display = "none";
  289. }
  290. export function openMenu() {
  291. const menuBg = document.querySelector("#betterytm-menu-bg") as HTMLElement;
  292. menuBg.style.visibility = "visible";
  293. menuBg.style.display = "block";
  294. }
  295. //#MARKER watermark
  296. /**
  297. * Adds a watermark beneath the logo
  298. */
  299. export function addWatermark() {
  300. const watermark = document.createElement("span");
  301. watermark.id = "betterytm-watermark";
  302. watermark.className = "style-scope ytmusic-nav-bar";
  303. watermark.innerText = info.name;
  304. watermark.title = "Open menu";
  305. watermark.addEventListener("click", () => openMenu());
  306. const style = `\
  307. #betterytm-watermark {
  308. font-size: 10px;
  309. display: inline-block;
  310. position: absolute;
  311. left: 45px;
  312. top: 46px;
  313. z-index: 10;
  314. color: white;
  315. text-decoration: none;
  316. cursor: pointer;
  317. }
  318. @media(max-width: 615px) {
  319. #betterytm-watermark {
  320. display: none;
  321. }
  322. }
  323. #betterytm-watermark:hover {
  324. text-decoration: underline;
  325. }`;
  326. addGlobalStyle(style, "watermark");
  327. const logoElem = document.querySelector("#left-content") as HTMLElement;
  328. insertAfter(logoElem, watermark);
  329. dbg && console.log("BetterYTM: Added watermark element:", watermark);
  330. }
  331. //#MARKER remove upgrade tab
  332. let removeUpgradeTries = 0;
  333. /** Removes the "Upgrade" / YT Music Premium tab from the title / nav bar */
  334. export function removeUpgradeTab() {
  335. const tabElem = document.querySelector(".ytmusic-nav-bar ytmusic-pivot-bar-item-renderer[tab-id=\"SPunlimited\"]");
  336. if(tabElem) {
  337. tabElem.remove();
  338. dbg && console.log(`BetterYTM: Removed upgrade tab after ${removeUpgradeTries} tries`);
  339. }
  340. else if(removeUpgradeTries < triesLimit) {
  341. setTimeout(removeUpgradeTab, triesInterval); // TODO: improve this
  342. removeUpgradeTries++;
  343. }
  344. else
  345. console.error(`BetterYTM: Couldn't find upgrade tab to remove after ${removeUpgradeTries} tries`);
  346. }
  347. // #SECTION volume slider
  348. /** Sets the volume slider to a set size */
  349. export function setVolSliderSize() {
  350. const { volumeSliderSize: size } = features;
  351. if(typeof size !== "number" || isNaN(Number(size)))
  352. return;
  353. const style = `\
  354. .volume-slider.ytmusic-player-bar, .expand-volume-slider.ytmusic-player-bar {
  355. width: ${size}px !important;
  356. }`;
  357. addGlobalStyle(style, "vol_slider_size");
  358. }
  359. /** Sets the `step` attribute of the volume slider */
  360. export function setVolSliderStep() {
  361. const sliderElem = document.querySelector("tp-yt-paper-slider#volume-slider") as HTMLInputElement;
  362. sliderElem.setAttribute("step", String(features.volumeSliderStep));
  363. }