menu.ts 12 KB

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