menu_old.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936
  1. import { debounce, isScrollable } from "@sv443-network/userutils";
  2. import { defaultConfig, getFeatures, migrations, saveFeatures, setDefaultFeatures } from "../config";
  3. import { scriptInfo } from "../constants";
  4. import { FeatureCategory, FeatInfoKey, categoryNames, featInfo } from "../features/index";
  5. import { getResourceUrl, info, log, warn } from "../utils";
  6. import { formatVersion } from "../config";
  7. import { siteEvents } from "../events";
  8. import { FeatureConfig } from "../types";
  9. import changelogContent from "../../changelog.md";
  10. import "./menu_old.css";
  11. //#MARKER create menu elements
  12. export let isMenuOpen = false;
  13. /** Threshold in pixels from the top of the options container that dictates for how long the scroll indicator is shown */
  14. const scrollIndicatorOffsetThreshold = 30;
  15. let scrollIndicatorEnabled = true;
  16. /**
  17. * Adds an element to open the BetterYTM menu
  18. * @deprecated to be replaced with new menu - see https://github.com/Sv443/BetterYTM/issues/23
  19. */
  20. export async function addMenu() {
  21. //#SECTION backdrop & menu container
  22. const backgroundElem = document.createElement("div");
  23. backgroundElem.id = "bytm-cfg-menu-bg";
  24. backgroundElem.classList.add("bytm-menu-bg");
  25. backgroundElem.title = "Click here to close the menu";
  26. backgroundElem.style.visibility = "hidden";
  27. backgroundElem.style.display = "none";
  28. backgroundElem.addEventListener("click", (e) => {
  29. if(isMenuOpen && (e.target as HTMLElement)?.id === "bytm-cfg-menu-bg")
  30. closeMenu(e);
  31. });
  32. document.body.addEventListener("keydown", (e) => {
  33. if(isMenuOpen && e.key === "Escape")
  34. closeMenu(e);
  35. });
  36. const menuContainer = document.createElement("div");
  37. menuContainer.title = ""; // prevent bg title from propagating downwards
  38. menuContainer.classList.add("bytm-menu");
  39. menuContainer.id = "bytm-cfg-menu";
  40. //#SECTION title bar
  41. const headerElem = document.createElement("div");
  42. headerElem.classList.add("bytm-menu-header");
  43. const titleCont = document.createElement("div");
  44. titleCont.id = "bytm-menu-titlecont";
  45. titleCont.role = "heading";
  46. titleCont.ariaLevel = "1";
  47. const titleElem = document.createElement("h2");
  48. titleElem.id = "bytm-menu-title";
  49. titleElem.innerText = `${scriptInfo.name} - Configuration`;
  50. const linksCont = document.createElement("div");
  51. linksCont.id = "bytm-menu-linkscont";
  52. const addLink = (imgSrc: string, href: string, title: string) => {
  53. const anchorElem = document.createElement("a");
  54. anchorElem.className = "bytm-menu-link bytm-no-select";
  55. anchorElem.rel = "noopener noreferrer";
  56. anchorElem.target = "_blank";
  57. anchorElem.href = href;
  58. anchorElem.title = title;
  59. const imgElem = document.createElement("img");
  60. imgElem.className = "bytm-menu-img";
  61. imgElem.src = imgSrc;
  62. imgElem.style.width = "32px";
  63. imgElem.style.height = "32px";
  64. anchorElem.appendChild(imgElem);
  65. linksCont.appendChild(anchorElem);
  66. };
  67. addLink(await getResourceUrl("github"), scriptInfo.namespace, `Open ${scriptInfo.name} on GitHub`);
  68. // TODO:
  69. // addLink(await getResourceUrl("greasyfork"), "https://greasyfork.org/en/users/184165-sv443", `Open ${scriptInfo.name} on GreasyFork`);
  70. const closeElem = document.createElement("img");
  71. closeElem.classList.add("bytm-menu-close");
  72. closeElem.src = await getResourceUrl("close");
  73. closeElem.title = "Click to close the menu";
  74. closeElem.addEventListener("click", closeMenu);
  75. titleCont.appendChild(titleElem);
  76. titleCont.appendChild(linksCont);
  77. headerElem.appendChild(titleCont);
  78. headerElem.appendChild(closeElem);
  79. //#SECTION feature list
  80. const featuresCont = document.createElement("div");
  81. featuresCont.id = "bytm-menu-opts";
  82. /** Gets called whenever the feature config is changed */
  83. const confChanged = debounce(async (key: keyof typeof defaultConfig, initialVal: number | boolean | Record<string, unknown>, newVal: number | boolean | Record<string, unknown>) => {
  84. const fmt = (val: unknown) => typeof val === "object" ? JSON.stringify(val) : String(val);
  85. info(`Feature config changed at key '${key}', from value '${fmt(initialVal)}' to '${fmt(newVal)}'`);
  86. const featConf = { ...getFeatures() };
  87. featConf[key] = newVal as never;
  88. await saveFeatures(featConf);
  89. });
  90. const featureCfg = getFeatures();
  91. const featureCfgWithCategories = Object.entries(featInfo)
  92. .reduce(
  93. (acc, [key, { category }]) => {
  94. if(!acc[category])
  95. acc[category] = {} as Record<FeatInfoKey, unknown>;
  96. acc[category][key as FeatInfoKey] = featureCfg[key as FeatInfoKey];
  97. return acc;
  98. },
  99. {} as Record<FeatureCategory, Record<FeatInfoKey, unknown>>,
  100. );
  101. const fmtVal = (v: unknown) => String(v).trim();
  102. const toggleLabelText = (toggled: boolean) => toggled ? "On" : "Off";
  103. for(const category in featureCfgWithCategories) {
  104. const featObj = featureCfgWithCategories[category as FeatureCategory];
  105. const catHeaderElem = document.createElement("h3");
  106. catHeaderElem.classList.add("bytm-ftconf-category-header");
  107. catHeaderElem.role = "heading";
  108. catHeaderElem.ariaLevel = "2";
  109. catHeaderElem.innerText = `${categoryNames[category as FeatureCategory]}:`;
  110. featuresCont.appendChild(catHeaderElem);
  111. for(const featKey in featObj) {
  112. const ftInfo = featInfo[featKey as keyof typeof featureCfg];
  113. // @ts-ignore
  114. if(!ftInfo || ftInfo.hidden === true)
  115. continue;
  116. const { desc, type, default: ftDefault } = ftInfo;
  117. // @ts-ignore
  118. const step = ftInfo?.step ?? undefined;
  119. const val = featureCfg[featKey as keyof typeof featureCfg];
  120. const initialVal = val ?? ftDefault ?? undefined;
  121. const ftConfElem = document.createElement("div");
  122. ftConfElem.classList.add("bytm-ftitem");
  123. {
  124. const textElem = document.createElement("div");
  125. textElem.innerText = desc;
  126. ftConfElem.appendChild(textElem);
  127. }
  128. {
  129. let inputType: string | undefined = "text";
  130. let inputTag = "input";
  131. switch(type)
  132. {
  133. case "toggle":
  134. inputType = "checkbox";
  135. break;
  136. case "slider":
  137. inputType = "range";
  138. break;
  139. case "number":
  140. inputType = "number";
  141. break;
  142. case "select":
  143. inputTag = "select";
  144. inputType = undefined;
  145. break;
  146. }
  147. const inputElemId = `bytm-ftconf-${featKey}-input`;
  148. const ctrlElem = document.createElement("span");
  149. ctrlElem.classList.add("bytm-ftconf-ctrl");
  150. const inputElem = document.createElement(inputTag) as HTMLInputElement;
  151. inputElem.classList.add("bytm-ftconf-input");
  152. inputElem.id = inputElemId;
  153. if(inputType)
  154. inputElem.type = inputType;
  155. if(typeof initialVal !== "undefined")
  156. inputElem.value = String(initialVal);
  157. if(type === "number" && step)
  158. inputElem.step = step;
  159. // @ts-ignore
  160. if(typeof ftInfo.min !== "undefined" && ftInfo.max !== "undefined") {
  161. // @ts-ignore
  162. inputElem.min = ftInfo.min;
  163. // @ts-ignore
  164. inputElem.max = ftInfo.max;
  165. }
  166. if(type === "toggle" && typeof initialVal !== "undefined")
  167. inputElem.checked = Boolean(initialVal);
  168. // @ts-ignore
  169. const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
  170. let labelElem: HTMLLabelElement | undefined;
  171. if(type === "slider") {
  172. labelElem = document.createElement("label");
  173. labelElem.classList.add("bytm-ftconf-label", "bytm-slider-label");
  174. labelElem.htmlFor = inputElemId;
  175. labelElem.innerText = fmtVal(initialVal) + unitTxt;
  176. inputElem.addEventListener("input", () => {
  177. if(labelElem)
  178. labelElem.innerText = fmtVal(parseInt(inputElem.value)) + unitTxt;
  179. });
  180. }
  181. else if(type === "toggle") {
  182. labelElem = document.createElement("label");
  183. labelElem.classList.add("bytm-ftconf-label", "bytm-toggle-label");
  184. labelElem.htmlFor = inputElemId;
  185. labelElem.innerText = toggleLabelText(Boolean(initialVal)) + unitTxt;
  186. inputElem.addEventListener("input", () => {
  187. if(labelElem)
  188. labelElem.innerText = toggleLabelText(inputElem.checked) + unitTxt;
  189. });
  190. }
  191. else if(type === "select") {
  192. for(const { value, label } of ftInfo.options) {
  193. const optionElem = document.createElement("option");
  194. optionElem.value = String(value);
  195. optionElem.innerText = label;
  196. if(value === initialVal)
  197. optionElem.selected = true;
  198. inputElem.appendChild(optionElem);
  199. }
  200. }
  201. inputElem.addEventListener("input", () => {
  202. let v = Number(String(inputElem.value).trim());
  203. if(isNaN(v))
  204. v = Number(inputElem.value);
  205. if(typeof initialVal !== "undefined")
  206. confChanged(featKey as keyof FeatureConfig, initialVal, (type !== "toggle" ? v : inputElem.checked));
  207. });
  208. if(labelElem) {
  209. labelElem.id = `bytm-ftconf-${featKey}-label`;
  210. ctrlElem.appendChild(labelElem);
  211. }
  212. ctrlElem.appendChild(inputElem);
  213. ftConfElem.appendChild(ctrlElem);
  214. }
  215. featuresCont.appendChild(ftConfElem);
  216. }
  217. }
  218. //#SECTION set values of inputs on external change
  219. siteEvents.on("rebuildCfgMenu", (newConfig) => {
  220. for(const ftKey in featInfo) {
  221. const ftElem = document.querySelector<HTMLInputElement>(`#bytm-ftconf-${ftKey}-input`);
  222. const labelElem = document.querySelector<HTMLLabelElement>(`#bytm-ftconf-${ftKey}-label`);
  223. if(!ftElem)
  224. continue;
  225. const ftInfo = featInfo[ftKey as keyof typeof featInfo];
  226. const value = newConfig[ftKey as keyof FeatureConfig];
  227. if(ftInfo.type === "toggle")
  228. ftElem.checked = Boolean(value);
  229. else
  230. ftElem.value = String(value);
  231. if(!labelElem)
  232. continue;
  233. // @ts-ignore
  234. const unitTxt = typeof ftInfo.unit === "string" ? " " + ftInfo.unit : "";
  235. if(ftInfo.type === "slider")
  236. labelElem.innerText = fmtVal(Number(value)) + unitTxt;
  237. else if(ftInfo.type === "toggle")
  238. labelElem.innerText = toggleLabelText(Boolean(value)) + unitTxt;
  239. }
  240. });
  241. //#SECTION scroll indicator
  242. const scrollIndicator = document.createElement("img");
  243. scrollIndicator.id = "bytm-menu-scroll-indicator";
  244. scrollIndicator.src = await getResourceUrl("arrow_down");
  245. scrollIndicator.role = "button";
  246. scrollIndicator.title = "Click to scroll to the bottom";
  247. featuresCont.appendChild(scrollIndicator);
  248. scrollIndicator.addEventListener("click", () => {
  249. const bottomAnchor = document.querySelector("#bytm-menu-bottom-anchor");
  250. bottomAnchor?.scrollIntoView({
  251. behavior: "smooth",
  252. });
  253. });
  254. featuresCont.addEventListener("scroll", (evt: Event) => {
  255. const scrollPos = (evt.target as HTMLDivElement)?.scrollTop ?? 0;
  256. const scrollIndicator = document.querySelector<HTMLImageElement>("#bytm-menu-scroll-indicator");
  257. if(!scrollIndicator)
  258. return;
  259. if(scrollIndicatorEnabled && scrollPos > scrollIndicatorOffsetThreshold && !scrollIndicator.classList.contains("bytm-hidden")) {
  260. scrollIndicator.classList.add("bytm-hidden");
  261. }
  262. else if(scrollIndicatorEnabled && scrollPos <= scrollIndicatorOffsetThreshold && scrollIndicator.classList.contains("bytm-hidden")) {
  263. scrollIndicator.classList.remove("bytm-hidden");
  264. }
  265. });
  266. const bottomAnchor = document.createElement("div");
  267. bottomAnchor.id = "bytm-menu-bottom-anchor";
  268. featuresCont.appendChild(bottomAnchor);
  269. //#SECTION footer
  270. const footerCont = document.createElement("div");
  271. footerCont.id = "bytm-menu-footer-cont";
  272. const footerElem = document.createElement("div");
  273. footerElem.classList.add("bytm-menu-footer");
  274. footerElem.innerText = "You need to reload the page to apply changes";
  275. const reloadElem = document.createElement("button");
  276. reloadElem.classList.add("bytm-btn");
  277. reloadElem.style.marginLeft = "10px";
  278. reloadElem.innerText = "Reload now";
  279. reloadElem.title = "Click to reload the page";
  280. reloadElem.addEventListener("click", () => {
  281. closeMenu();
  282. location.reload();
  283. });
  284. footerElem.appendChild(reloadElem);
  285. const resetElem = document.createElement("button");
  286. resetElem.classList.add("bytm-btn");
  287. resetElem.title = "Click to reset all settings to their default values";
  288. resetElem.innerText = "Reset";
  289. resetElem.addEventListener("click", async () => {
  290. if(confirm("Do you really want to reset all settings to their default values?\nThe page will be automatically reloaded.")) {
  291. await setDefaultFeatures();
  292. closeMenu();
  293. location.reload();
  294. }
  295. });
  296. const exportElem = document.createElement("button");
  297. exportElem.classList.add("bytm-btn");
  298. exportElem.title = "Click to export your current configuration";
  299. exportElem.innerText = "Export";
  300. exportElem.addEventListener("click", async () => {
  301. closeMenu();
  302. openExportMenu();
  303. });
  304. const importElem = document.createElement("button");
  305. importElem.classList.add("bytm-btn");
  306. importElem.title = "Click to import a configuration you have previously exported";
  307. importElem.innerText = "Import";
  308. importElem.addEventListener("click", async () => {
  309. closeMenu();
  310. openImportMenu();
  311. });
  312. const buttonsCont = document.createElement("div");
  313. buttonsCont.id = "bytm-menu-footer-buttons-cont";
  314. buttonsCont.appendChild(exportElem);
  315. buttonsCont.appendChild(importElem);
  316. buttonsCont.appendChild(resetElem);
  317. footerCont.appendChild(footerElem);
  318. footerCont.appendChild(buttonsCont);
  319. //#SECTION finalize
  320. menuContainer.appendChild(headerElem);
  321. menuContainer.appendChild(featuresCont);
  322. const versionCont = document.createElement("div");
  323. versionCont.id = "bytm-menu-version-cont";
  324. const versionElem = document.createElement("a");
  325. versionElem.id = "bytm-menu-version";
  326. versionElem.role = "button";
  327. versionElem.title = `Version ${scriptInfo.version} (build ${scriptInfo.lastCommit}) - click to open the changelog`;
  328. versionElem.innerText = `v${scriptInfo.version} (${scriptInfo.lastCommit})`;
  329. versionElem.addEventListener("click", (e) => {
  330. e.preventDefault();
  331. e.stopPropagation();
  332. closeMenu();
  333. openChangelogMenu();
  334. });
  335. versionCont.appendChild(versionElem);
  336. menuContainer.appendChild(footerCont);
  337. menuContainer.appendChild(versionCont);
  338. backgroundElem.appendChild(menuContainer);
  339. document.body.appendChild(backgroundElem);
  340. window.addEventListener("resize", debounce(checkToggleScrollIndicator, 150));
  341. await addChangelogMenu();
  342. await addExportMenu();
  343. await addImportMenu();
  344. log("Added menu element");
  345. }
  346. /** Closes the menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  347. export function closeMenu(evt?: MouseEvent | KeyboardEvent) {
  348. if(!isMenuOpen)
  349. return;
  350. isMenuOpen = false;
  351. evt?.bubbles && evt.stopPropagation();
  352. document.body.classList.remove("bytm-disable-scroll");
  353. const menuBg = document.querySelector("#bytm-cfg-menu-bg") as HTMLElement;
  354. menuBg.style.visibility = "hidden";
  355. menuBg.style.display = "none";
  356. }
  357. /** Opens the menu if it is closed */
  358. export function openMenu() {
  359. if(isMenuOpen)
  360. return;
  361. isMenuOpen = true;
  362. document.body.classList.add("bytm-disable-scroll");
  363. const menuBg = document.querySelector("#bytm-cfg-menu-bg") as HTMLElement;
  364. menuBg.style.visibility = "visible";
  365. menuBg.style.display = "block";
  366. checkToggleScrollIndicator();
  367. }
  368. /** Checks if the features container is scrollable and toggles the scroll indicator accordingly */
  369. function checkToggleScrollIndicator() {
  370. const featuresCont = document.querySelector<HTMLElement>("#bytm-menu-opts");
  371. const scrollIndicator = document.querySelector<HTMLElement>("#bytm-menu-scroll-indicator");
  372. // disable scroll indicator if container doesn't scroll
  373. if(featuresCont && scrollIndicator) {
  374. const verticalScroll = isScrollable(featuresCont).vertical;
  375. /** If true, the indicator's threshold is under the available scrollable space and so it should be disabled */
  376. const underThreshold = featuresCont.scrollHeight - featuresCont.clientHeight <= scrollIndicatorOffsetThreshold;
  377. if(!underThreshold && verticalScroll && !scrollIndicatorEnabled) {
  378. scrollIndicatorEnabled = true;
  379. scrollIndicator.classList.remove("bytm-hidden");
  380. }
  381. if((!verticalScroll && scrollIndicatorEnabled) || underThreshold) {
  382. scrollIndicatorEnabled = false;
  383. scrollIndicator.classList.add("bytm-hidden");
  384. }
  385. }
  386. }
  387. //#MARKER export menu
  388. let isExportMenuOpen = false;
  389. /** Adds a menu to copy the current configuration as JSON (hidden by default) */
  390. async function addExportMenu() {
  391. const menuBgElem = document.createElement("div");
  392. menuBgElem.id = "bytm-export-menu-bg";
  393. menuBgElem.classList.add("bytm-menu-bg");
  394. menuBgElem.title = "Click here to close the menu";
  395. menuBgElem.style.visibility = "hidden";
  396. menuBgElem.style.display = "none";
  397. menuBgElem.addEventListener("click", (e) => {
  398. if(isExportMenuOpen && (e.target as HTMLElement)?.id === "bytm-export-menu-bg") {
  399. closeExportMenu(e);
  400. openMenu();
  401. }
  402. });
  403. document.body.addEventListener("keydown", (e) => {
  404. if(isExportMenuOpen && e.key === "Escape") {
  405. closeExportMenu(e);
  406. openMenu();
  407. }
  408. });
  409. const menuContainer = document.createElement("div");
  410. menuContainer.title = ""; // prevent bg title from propagating downwards
  411. menuContainer.classList.add("bytm-menu");
  412. menuContainer.id = "bytm-export-menu";
  413. //#SECTION title bar
  414. const headerElem = document.createElement("div");
  415. headerElem.classList.add("bytm-menu-header");
  416. const titleCont = document.createElement("div");
  417. titleCont.id = "bytm-menu-titlecont";
  418. titleCont.role = "heading";
  419. titleCont.ariaLevel = "1";
  420. const titleElem = document.createElement("h2");
  421. titleElem.id = "bytm-menu-title";
  422. titleElem.innerText = `${scriptInfo.name} - Export Configuration`;
  423. const closeElem = document.createElement("img");
  424. closeElem.classList.add("bytm-menu-close");
  425. closeElem.src = await getResourceUrl("close");
  426. closeElem.title = "Click to close the menu";
  427. closeElem.addEventListener("click", (e) => {
  428. closeExportMenu(e);
  429. openMenu();
  430. });
  431. titleCont.appendChild(titleElem);
  432. headerElem.appendChild(titleCont);
  433. headerElem.appendChild(closeElem);
  434. //#SECTION body
  435. const menuBodyElem = document.createElement("div");
  436. menuBodyElem.classList.add("bytm-menu-body");
  437. const textElem = document.createElement("div");
  438. textElem.id = "bytm-export-menu-text";
  439. textElem.innerText = "Copy the following text to export your configuration:";
  440. const textAreaElem = document.createElement("textarea");
  441. textAreaElem.id = "bytm-export-menu-textarea";
  442. textAreaElem.readOnly = true;
  443. textAreaElem.value = JSON.stringify({ formatVersion, data: getFeatures() });
  444. siteEvents.on("configChanged", (data) => {
  445. const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-export-menu-textarea");
  446. if(textAreaElem)
  447. textAreaElem.value = JSON.stringify({ formatVersion, data });
  448. });
  449. //#SECTION footer
  450. const footerElem = document.createElement("div");
  451. footerElem.classList.add("bytm-menu-footer-right");
  452. const copyBtnElem = document.createElement("button");
  453. copyBtnElem.classList.add("bytm-btn");
  454. copyBtnElem.innerText = "Copy to clipboard";
  455. copyBtnElem.title = "Click to copy the configuration to your clipboard";
  456. const copiedTextElem = document.createElement("span");
  457. copiedTextElem.classList.add("bytm-menu-footer-copied");
  458. copiedTextElem.innerText = "Copied!";
  459. copiedTextElem.style.display = "none";
  460. copyBtnElem.addEventListener("click", async (evt) => {
  461. evt?.bubbles && evt.stopPropagation();
  462. const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-export-menu-textarea");
  463. if(textAreaElem) {
  464. GM.setClipboard(textAreaElem.value);
  465. copiedTextElem.style.display = "inline-block";
  466. setTimeout(() => {
  467. copiedTextElem.style.display = "none";
  468. }, 3000);
  469. }
  470. });
  471. // flex-direction is row-reverse
  472. footerElem.appendChild(copyBtnElem);
  473. footerElem.appendChild(copiedTextElem);
  474. //#SECTION finalize
  475. menuBodyElem.appendChild(textElem);
  476. menuBodyElem.appendChild(textAreaElem);
  477. menuBodyElem.appendChild(footerElem);
  478. menuContainer.appendChild(headerElem);
  479. menuContainer.appendChild(menuBodyElem);
  480. menuBgElem.appendChild(menuContainer);
  481. document.body.appendChild(menuBgElem);
  482. }
  483. /** Closes the export menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  484. function closeExportMenu(evt: MouseEvent | KeyboardEvent) {
  485. if(!isExportMenuOpen)
  486. return;
  487. isExportMenuOpen = false;
  488. evt?.bubbles && evt.stopPropagation();
  489. document.body.classList.remove("bytm-disable-scroll");
  490. const menuBg = document.querySelector<HTMLElement>("#bytm-export-menu-bg");
  491. if(!menuBg)
  492. return warn("Couldn't find export menu background element");
  493. menuBg.style.visibility = "hidden";
  494. menuBg.style.display = "none";
  495. }
  496. /** Opens the export menu if it is closed */
  497. function openExportMenu() {
  498. if(isExportMenuOpen)
  499. return;
  500. isExportMenuOpen = true;
  501. document.body.classList.add("bytm-disable-scroll");
  502. const menuBg = document.querySelector<HTMLElement>("#bytm-export-menu-bg");
  503. if(!menuBg)
  504. return warn("Couldn't find export menu background element");
  505. menuBg.style.visibility = "visible";
  506. menuBg.style.display = "block";
  507. }
  508. //#MARKER import menu
  509. let isImportMenuOpen = false;
  510. /** Adds a menu to import a configuration from JSON (hidden by default) */
  511. async function addImportMenu() {
  512. const menuBgElem = document.createElement("div");
  513. menuBgElem.id = "bytm-import-menu-bg";
  514. menuBgElem.classList.add("bytm-menu-bg");
  515. menuBgElem.title = "Click here to close the menu";
  516. menuBgElem.style.visibility = "hidden";
  517. menuBgElem.style.display = "none";
  518. menuBgElem.addEventListener("click", (e) => {
  519. if(isImportMenuOpen && (e.target as HTMLElement)?.id === "bytm-import-menu-bg") {
  520. closeImportMenu(e);
  521. openMenu();
  522. }
  523. });
  524. document.body.addEventListener("keydown", (e) => {
  525. if(isImportMenuOpen && e.key === "Escape") {
  526. closeImportMenu(e);
  527. openMenu();
  528. }
  529. });
  530. const menuContainer = document.createElement("div");
  531. menuContainer.title = ""; // prevent bg title from propagating downwards
  532. menuContainer.classList.add("bytm-menu");
  533. menuContainer.id = "bytm-import-menu";
  534. //#SECTION title bar
  535. const headerElem = document.createElement("div");
  536. headerElem.classList.add("bytm-menu-header");
  537. const titleCont = document.createElement("div");
  538. titleCont.id = "bytm-menu-titlecont";
  539. titleCont.role = "heading";
  540. titleCont.ariaLevel = "1";
  541. const titleElem = document.createElement("h2");
  542. titleElem.id = "bytm-menu-title";
  543. titleElem.innerText = `${scriptInfo.name} - Import Configuration`;
  544. const closeElem = document.createElement("img");
  545. closeElem.classList.add("bytm-menu-close");
  546. closeElem.src = await getResourceUrl("close");
  547. closeElem.title = "Click to close the menu";
  548. closeElem.addEventListener("click", (e) => {
  549. closeImportMenu(e);
  550. openMenu();
  551. });
  552. titleCont.appendChild(titleElem);
  553. headerElem.appendChild(titleCont);
  554. headerElem.appendChild(closeElem);
  555. //#SECTION body
  556. const menuBodyElem = document.createElement("div");
  557. menuBodyElem.classList.add("bytm-menu-body");
  558. const textElem = document.createElement("div");
  559. textElem.id = "bytm-import-menu-text";
  560. textElem.innerText = "Paste the configuration you want to import into the field below, then click the import button";
  561. const textAreaElem = document.createElement("textarea");
  562. textAreaElem.id = "bytm-import-menu-textarea";
  563. //#SECTION footer
  564. const footerElem = document.createElement("div");
  565. footerElem.classList.add("bytm-menu-footer-right");
  566. const importBtnElem = document.createElement("button");
  567. importBtnElem.classList.add("bytm-btn");
  568. importBtnElem.innerText = "Import";
  569. importBtnElem.title = "Click to import the configuration";
  570. importBtnElem.addEventListener("click", async (evt) => {
  571. evt?.bubbles && evt.stopPropagation();
  572. const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-import-menu-textarea");
  573. if(!textAreaElem)
  574. return warn("Couldn't find import menu textarea element");
  575. try {
  576. const parsed = JSON.parse(textAreaElem.value.trim());
  577. if(typeof parsed !== "object")
  578. return alert("The imported data is not an object");
  579. if(typeof parsed.formatVersion !== "number")
  580. return alert("The imported data does not contain a format version");
  581. if(typeof parsed.data !== "object")
  582. return alert("The imported object does not contain any data");
  583. if(parsed.formatVersion < formatVersion) {
  584. let newData = JSON.parse(JSON.stringify(parsed.data));
  585. const sortedMigrations = Object.entries(migrations)
  586. .sort(([a], [b]) => Number(a) - Number(b));
  587. let curFmtVer = Number(parsed.formatVersion);
  588. for(const [fmtVer, migrationFunc] of sortedMigrations) {
  589. const ver = Number(fmtVer);
  590. if(curFmtVer < formatVersion && curFmtVer < ver) {
  591. try {
  592. const migRes = JSON.parse(JSON.stringify(migrationFunc(newData)));
  593. newData = migRes instanceof Promise ? await migRes : migRes;
  594. curFmtVer = ver;
  595. }
  596. catch(err) {
  597. console.error(`Error while running migration function for format version ${fmtVer}:`, err);
  598. }
  599. }
  600. }
  601. parsed.formatVersion = curFmtVer;
  602. parsed.data = newData;
  603. }
  604. else if(parsed.formatVersion !== formatVersion)
  605. return alert(`The imported data is in an unsupported format version (expected ${formatVersion} or lower, got ${parsed.formatVersion})`);
  606. await saveFeatures(parsed.data);
  607. if(confirm("Successfully imported the configuration.\nDo you want to reload the page now to apply changes?"))
  608. return location.reload();
  609. siteEvents.emit("rebuildCfgMenu", parsed.data);
  610. closeImportMenu();
  611. openMenu();
  612. }
  613. catch(err) {
  614. warn("Couldn't import configuration:", err);
  615. alert("The imported data is not a valid configuration");
  616. }
  617. });
  618. footerElem.appendChild(importBtnElem);
  619. //#SECTION finalize
  620. menuBodyElem.appendChild(textElem);
  621. menuBodyElem.appendChild(textAreaElem);
  622. menuBodyElem.appendChild(footerElem);
  623. menuContainer.appendChild(headerElem);
  624. menuContainer.appendChild(menuBodyElem);
  625. menuBgElem.appendChild(menuContainer);
  626. document.body.appendChild(menuBgElem);
  627. }
  628. /** Closes the import menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  629. function closeImportMenu(evt?: MouseEvent | KeyboardEvent) {
  630. if(!isImportMenuOpen)
  631. return;
  632. isImportMenuOpen = false;
  633. evt?.bubbles && evt.stopPropagation();
  634. document.body.classList.remove("bytm-disable-scroll");
  635. const menuBg = document.querySelector<HTMLElement>("#bytm-import-menu-bg");
  636. const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-import-menu-textarea");
  637. if(textAreaElem)
  638. textAreaElem.value = "";
  639. if(!menuBg)
  640. return warn("Couldn't find import menu background element");
  641. menuBg.style.visibility = "hidden";
  642. menuBg.style.display = "none";
  643. }
  644. /** Opens the import menu if it is closed */
  645. function openImportMenu() {
  646. if(isImportMenuOpen)
  647. return;
  648. isImportMenuOpen = true;
  649. document.body.classList.add("bytm-disable-scroll");
  650. const menuBg = document.querySelector<HTMLElement>("#bytm-import-menu-bg");
  651. if(!menuBg)
  652. return warn("Couldn't find import menu background element");
  653. menuBg.style.visibility = "visible";
  654. menuBg.style.display = "block";
  655. }
  656. //#MARKER changelog menu
  657. let isChangelogMenuOpen = false;
  658. /** Adds a changelog menu (hidden by default) */
  659. async function addChangelogMenu() {
  660. const menuBgElem = document.createElement("div");
  661. menuBgElem.id = "bytm-changelog-menu-bg";
  662. menuBgElem.classList.add("bytm-menu-bg");
  663. menuBgElem.title = "Click here to close the menu";
  664. menuBgElem.style.visibility = "hidden";
  665. menuBgElem.style.display = "none";
  666. menuBgElem.addEventListener("click", (e) => {
  667. if(isChangelogMenuOpen && (e.target as HTMLElement)?.id === "bytm-changelog-menu-bg") {
  668. closeChangelogMenu(e);
  669. openMenu();
  670. }
  671. });
  672. document.body.addEventListener("keydown", (e) => {
  673. if(isChangelogMenuOpen && e.key === "Escape") {
  674. closeChangelogMenu(e);
  675. openMenu();
  676. }
  677. });
  678. const menuContainer = document.createElement("div");
  679. menuContainer.title = ""; // prevent bg title from propagating downwards
  680. menuContainer.classList.add("bytm-menu");
  681. menuContainer.id = "bytm-changelog-menu";
  682. //#SECTION title bar
  683. const headerElem = document.createElement("div");
  684. headerElem.classList.add("bytm-menu-header");
  685. const titleCont = document.createElement("div");
  686. titleCont.id = "bytm-menu-titlecont";
  687. titleCont.role = "heading";
  688. titleCont.ariaLevel = "1";
  689. const titleElem = document.createElement("h2");
  690. titleElem.id = "bytm-menu-title";
  691. titleElem.innerText = `${scriptInfo.name} - Changelog`;
  692. const closeElem = document.createElement("img");
  693. closeElem.classList.add("bytm-menu-close");
  694. closeElem.src = await getResourceUrl("close");
  695. closeElem.title = "Click to close the menu";
  696. closeElem.addEventListener("click", (e) => {
  697. closeChangelogMenu(e);
  698. openMenu();
  699. });
  700. titleCont.appendChild(titleElem);
  701. headerElem.appendChild(titleCont);
  702. headerElem.appendChild(closeElem);
  703. //#SECTION body
  704. const menuBodyElem = document.createElement("div");
  705. menuBodyElem.id = "bytm-changelog-menu-body";
  706. menuBodyElem.classList.add("bytm-menu-body");
  707. const textElem = document.createElement("div");
  708. textElem.id = "bytm-changelog-menu-text";
  709. textElem.classList.add("bytm-markdown-container");
  710. textElem.innerHTML = changelogContent;
  711. //#SECTION finalize
  712. menuBodyElem.appendChild(textElem);
  713. menuContainer.appendChild(headerElem);
  714. menuContainer.appendChild(menuBodyElem);
  715. menuBgElem.appendChild(menuContainer);
  716. document.body.appendChild(menuBgElem);
  717. const anchors = document.querySelectorAll<HTMLAnchorElement>("#bytm-changelog-menu-text a");
  718. for(const anchor of anchors)
  719. anchor.target = "_blank";
  720. }
  721. /** Closes the changelog menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  722. function closeChangelogMenu(evt?: MouseEvent | KeyboardEvent) {
  723. if(!isChangelogMenuOpen)
  724. return;
  725. isChangelogMenuOpen = false;
  726. evt?.bubbles && evt.stopPropagation();
  727. document.body.classList.remove("bytm-disable-scroll");
  728. const menuBg = document.querySelector<HTMLElement>("#bytm-changelog-menu-bg");
  729. if(!menuBg)
  730. return warn("Couldn't find changelog menu background element");
  731. menuBg.style.visibility = "hidden";
  732. menuBg.style.display = "none";
  733. }
  734. /** Opens the changelog menu if it is closed */
  735. function openChangelogMenu() {
  736. if(isChangelogMenuOpen)
  737. return;
  738. isChangelogMenuOpen = true;
  739. document.body.classList.add("bytm-disable-scroll");
  740. const menuBg = document.querySelector<HTMLElement>("#bytm-changelog-menu-bg");
  741. if(!menuBg)
  742. return warn("Couldn't find changelog menu background element");
  743. menuBg.style.visibility = "visible";
  744. menuBg.style.display = "block";
  745. }