menu_old.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942
  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. addLink(await getResourceUrl("greasyfork"), "https://greasyfork.org/en/scripts/475682-betterytm", `Open ${scriptInfo.name} on GreasyFork`);
  69. addLink(await getResourceUrl("openuserjs"), "https://openuserjs.org/scripts/Sv443/BetterYTM", `Open ${scriptInfo.name} on OpenUserJS`);
  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.buildNumber}) - click to open the changelog`;
  328. versionElem.innerText = `v${scriptInfo.version} (${scriptInfo.buildNumber})`;
  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<HTMLElement>("#bytm-cfg-menu-bg");
  354. if(!menuBg)
  355. return;
  356. menuBg.style.visibility = "hidden";
  357. menuBg.style.display = "none";
  358. }
  359. /** Opens the menu if it is closed */
  360. export function openMenu() {
  361. if(isMenuOpen)
  362. return;
  363. isMenuOpen = true;
  364. document.body.classList.add("bytm-disable-scroll");
  365. const menuBg = document.querySelector<HTMLElement>("#bytm-cfg-menu-bg");
  366. if(!menuBg)
  367. return;
  368. menuBg.style.visibility = "visible";
  369. menuBg.style.display = "block";
  370. checkToggleScrollIndicator();
  371. }
  372. /** Checks if the features container is scrollable and toggles the scroll indicator accordingly */
  373. function checkToggleScrollIndicator() {
  374. const featuresCont = document.querySelector<HTMLElement>("#bytm-menu-opts");
  375. const scrollIndicator = document.querySelector<HTMLElement>("#bytm-menu-scroll-indicator");
  376. // disable scroll indicator if container doesn't scroll
  377. if(featuresCont && scrollIndicator) {
  378. const verticalScroll = isScrollable(featuresCont).vertical;
  379. /** If true, the indicator's threshold is under the available scrollable space and so it should be disabled */
  380. const underThreshold = featuresCont.scrollHeight - featuresCont.clientHeight <= scrollIndicatorOffsetThreshold;
  381. if(!underThreshold && verticalScroll && !scrollIndicatorEnabled) {
  382. scrollIndicatorEnabled = true;
  383. scrollIndicator.classList.remove("bytm-hidden");
  384. }
  385. if((!verticalScroll && scrollIndicatorEnabled) || underThreshold) {
  386. scrollIndicatorEnabled = false;
  387. scrollIndicator.classList.add("bytm-hidden");
  388. }
  389. }
  390. }
  391. //#MARKER export menu
  392. let isExportMenuOpen = false;
  393. /** Adds a menu to copy the current configuration as JSON (hidden by default) */
  394. async function addExportMenu() {
  395. const menuBgElem = document.createElement("div");
  396. menuBgElem.id = "bytm-export-menu-bg";
  397. menuBgElem.classList.add("bytm-menu-bg");
  398. menuBgElem.title = "Click here to close the menu";
  399. menuBgElem.style.visibility = "hidden";
  400. menuBgElem.style.display = "none";
  401. menuBgElem.addEventListener("click", (e) => {
  402. if(isExportMenuOpen && (e.target as HTMLElement)?.id === "bytm-export-menu-bg") {
  403. closeExportMenu(e);
  404. openMenu();
  405. }
  406. });
  407. document.body.addEventListener("keydown", (e) => {
  408. if(isExportMenuOpen && e.key === "Escape") {
  409. closeExportMenu(e);
  410. openMenu();
  411. }
  412. });
  413. const menuContainer = document.createElement("div");
  414. menuContainer.title = ""; // prevent bg title from propagating downwards
  415. menuContainer.classList.add("bytm-menu");
  416. menuContainer.id = "bytm-export-menu";
  417. //#SECTION title bar
  418. const headerElem = document.createElement("div");
  419. headerElem.classList.add("bytm-menu-header");
  420. const titleCont = document.createElement("div");
  421. titleCont.id = "bytm-menu-titlecont";
  422. titleCont.role = "heading";
  423. titleCont.ariaLevel = "1";
  424. const titleElem = document.createElement("h2");
  425. titleElem.id = "bytm-menu-title";
  426. titleElem.innerText = `${scriptInfo.name} - Export Configuration`;
  427. const closeElem = document.createElement("img");
  428. closeElem.classList.add("bytm-menu-close");
  429. closeElem.src = await getResourceUrl("close");
  430. closeElem.title = "Click to close the menu";
  431. closeElem.addEventListener("click", (e) => {
  432. closeExportMenu(e);
  433. openMenu();
  434. });
  435. titleCont.appendChild(titleElem);
  436. headerElem.appendChild(titleCont);
  437. headerElem.appendChild(closeElem);
  438. //#SECTION body
  439. const menuBodyElem = document.createElement("div");
  440. menuBodyElem.classList.add("bytm-menu-body");
  441. const textElem = document.createElement("div");
  442. textElem.id = "bytm-export-menu-text";
  443. textElem.innerText = "Copy the following text to export your configuration:";
  444. const textAreaElem = document.createElement("textarea");
  445. textAreaElem.id = "bytm-export-menu-textarea";
  446. textAreaElem.readOnly = true;
  447. textAreaElem.value = JSON.stringify({ formatVersion, data: getFeatures() });
  448. siteEvents.on("configChanged", (data) => {
  449. const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-export-menu-textarea");
  450. if(textAreaElem)
  451. textAreaElem.value = JSON.stringify({ formatVersion, data });
  452. });
  453. //#SECTION footer
  454. const footerElem = document.createElement("div");
  455. footerElem.classList.add("bytm-menu-footer-right");
  456. const copyBtnElem = document.createElement("button");
  457. copyBtnElem.classList.add("bytm-btn");
  458. copyBtnElem.innerText = "Copy to clipboard";
  459. copyBtnElem.title = "Click to copy the configuration to your clipboard";
  460. const copiedTextElem = document.createElement("span");
  461. copiedTextElem.classList.add("bytm-menu-footer-copied");
  462. copiedTextElem.innerText = "Copied!";
  463. copiedTextElem.style.display = "none";
  464. copyBtnElem.addEventListener("click", async (evt) => {
  465. evt?.bubbles && evt.stopPropagation();
  466. const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-export-menu-textarea");
  467. if(textAreaElem) {
  468. GM.setClipboard(textAreaElem.value);
  469. copiedTextElem.style.display = "inline-block";
  470. setTimeout(() => {
  471. copiedTextElem.style.display = "none";
  472. }, 3000);
  473. }
  474. });
  475. // flex-direction is row-reverse
  476. footerElem.appendChild(copyBtnElem);
  477. footerElem.appendChild(copiedTextElem);
  478. //#SECTION finalize
  479. menuBodyElem.appendChild(textElem);
  480. menuBodyElem.appendChild(textAreaElem);
  481. menuBodyElem.appendChild(footerElem);
  482. menuContainer.appendChild(headerElem);
  483. menuContainer.appendChild(menuBodyElem);
  484. menuBgElem.appendChild(menuContainer);
  485. document.body.appendChild(menuBgElem);
  486. }
  487. /** Closes the export menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  488. function closeExportMenu(evt: MouseEvent | KeyboardEvent) {
  489. if(!isExportMenuOpen)
  490. return;
  491. isExportMenuOpen = false;
  492. evt?.bubbles && evt.stopPropagation();
  493. document.body.classList.remove("bytm-disable-scroll");
  494. const menuBg = document.querySelector<HTMLElement>("#bytm-export-menu-bg");
  495. if(!menuBg)
  496. return warn("Couldn't find export menu background element");
  497. menuBg.style.visibility = "hidden";
  498. menuBg.style.display = "none";
  499. }
  500. /** Opens the export menu if it is closed */
  501. function openExportMenu() {
  502. if(isExportMenuOpen)
  503. return;
  504. isExportMenuOpen = true;
  505. document.body.classList.add("bytm-disable-scroll");
  506. const menuBg = document.querySelector<HTMLElement>("#bytm-export-menu-bg");
  507. if(!menuBg)
  508. return warn("Couldn't find export menu background element");
  509. menuBg.style.visibility = "visible";
  510. menuBg.style.display = "block";
  511. }
  512. //#MARKER import menu
  513. let isImportMenuOpen = false;
  514. /** Adds a menu to import a configuration from JSON (hidden by default) */
  515. async function addImportMenu() {
  516. const menuBgElem = document.createElement("div");
  517. menuBgElem.id = "bytm-import-menu-bg";
  518. menuBgElem.classList.add("bytm-menu-bg");
  519. menuBgElem.title = "Click here to close the menu";
  520. menuBgElem.style.visibility = "hidden";
  521. menuBgElem.style.display = "none";
  522. menuBgElem.addEventListener("click", (e) => {
  523. if(isImportMenuOpen && (e.target as HTMLElement)?.id === "bytm-import-menu-bg") {
  524. closeImportMenu(e);
  525. openMenu();
  526. }
  527. });
  528. document.body.addEventListener("keydown", (e) => {
  529. if(isImportMenuOpen && e.key === "Escape") {
  530. closeImportMenu(e);
  531. openMenu();
  532. }
  533. });
  534. const menuContainer = document.createElement("div");
  535. menuContainer.title = ""; // prevent bg title from propagating downwards
  536. menuContainer.classList.add("bytm-menu");
  537. menuContainer.id = "bytm-import-menu";
  538. //#SECTION title bar
  539. const headerElem = document.createElement("div");
  540. headerElem.classList.add("bytm-menu-header");
  541. const titleCont = document.createElement("div");
  542. titleCont.id = "bytm-menu-titlecont";
  543. titleCont.role = "heading";
  544. titleCont.ariaLevel = "1";
  545. const titleElem = document.createElement("h2");
  546. titleElem.id = "bytm-menu-title";
  547. titleElem.innerText = `${scriptInfo.name} - Import Configuration`;
  548. const closeElem = document.createElement("img");
  549. closeElem.classList.add("bytm-menu-close");
  550. closeElem.src = await getResourceUrl("close");
  551. closeElem.title = "Click to close the menu";
  552. closeElem.addEventListener("click", (e) => {
  553. closeImportMenu(e);
  554. openMenu();
  555. });
  556. titleCont.appendChild(titleElem);
  557. headerElem.appendChild(titleCont);
  558. headerElem.appendChild(closeElem);
  559. //#SECTION body
  560. const menuBodyElem = document.createElement("div");
  561. menuBodyElem.classList.add("bytm-menu-body");
  562. const textElem = document.createElement("div");
  563. textElem.id = "bytm-import-menu-text";
  564. textElem.innerText = "Paste the configuration you want to import into the field below, then click the import button";
  565. const textAreaElem = document.createElement("textarea");
  566. textAreaElem.id = "bytm-import-menu-textarea";
  567. //#SECTION footer
  568. const footerElem = document.createElement("div");
  569. footerElem.classList.add("bytm-menu-footer-right");
  570. const importBtnElem = document.createElement("button");
  571. importBtnElem.classList.add("bytm-btn");
  572. importBtnElem.innerText = "Import";
  573. importBtnElem.title = "Click to import the configuration";
  574. importBtnElem.addEventListener("click", async (evt) => {
  575. evt?.bubbles && evt.stopPropagation();
  576. const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-import-menu-textarea");
  577. if(!textAreaElem)
  578. return warn("Couldn't find import menu textarea element");
  579. try {
  580. const parsed = JSON.parse(textAreaElem.value.trim());
  581. if(typeof parsed !== "object")
  582. return alert("The imported data is not an object");
  583. if(typeof parsed.formatVersion !== "number")
  584. return alert("The imported data does not contain a format version");
  585. if(typeof parsed.data !== "object")
  586. return alert("The imported object does not contain any data");
  587. if(parsed.formatVersion < formatVersion) {
  588. let newData = JSON.parse(JSON.stringify(parsed.data));
  589. const sortedMigrations = Object.entries(migrations)
  590. .sort(([a], [b]) => Number(a) - Number(b));
  591. let curFmtVer = Number(parsed.formatVersion);
  592. for(const [fmtVer, migrationFunc] of sortedMigrations) {
  593. const ver = Number(fmtVer);
  594. if(curFmtVer < formatVersion && curFmtVer < ver) {
  595. try {
  596. const migRes = JSON.parse(JSON.stringify(migrationFunc(newData)));
  597. newData = migRes instanceof Promise ? await migRes : migRes;
  598. curFmtVer = ver;
  599. }
  600. catch(err) {
  601. console.error(`Error while running migration function for format version ${fmtVer}:`, err);
  602. }
  603. }
  604. }
  605. parsed.formatVersion = curFmtVer;
  606. parsed.data = newData;
  607. }
  608. else if(parsed.formatVersion !== formatVersion)
  609. return alert(`The imported data is in an unsupported format version (expected ${formatVersion} or lower, got ${parsed.formatVersion})`);
  610. await saveFeatures(parsed.data);
  611. if(confirm("Successfully imported the configuration.\nDo you want to reload the page now to apply changes?"))
  612. return location.reload();
  613. siteEvents.emit("rebuildCfgMenu", parsed.data);
  614. closeImportMenu();
  615. openMenu();
  616. }
  617. catch(err) {
  618. warn("Couldn't import configuration:", err);
  619. alert("The imported data is not a valid configuration");
  620. }
  621. });
  622. footerElem.appendChild(importBtnElem);
  623. //#SECTION finalize
  624. menuBodyElem.appendChild(textElem);
  625. menuBodyElem.appendChild(textAreaElem);
  626. menuBodyElem.appendChild(footerElem);
  627. menuContainer.appendChild(headerElem);
  628. menuContainer.appendChild(menuBodyElem);
  629. menuBgElem.appendChild(menuContainer);
  630. document.body.appendChild(menuBgElem);
  631. }
  632. /** Closes the import menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  633. function closeImportMenu(evt?: MouseEvent | KeyboardEvent) {
  634. if(!isImportMenuOpen)
  635. return;
  636. isImportMenuOpen = false;
  637. evt?.bubbles && evt.stopPropagation();
  638. document.body.classList.remove("bytm-disable-scroll");
  639. const menuBg = document.querySelector<HTMLElement>("#bytm-import-menu-bg");
  640. const textAreaElem = document.querySelector<HTMLTextAreaElement>("#bytm-import-menu-textarea");
  641. if(textAreaElem)
  642. textAreaElem.value = "";
  643. if(!menuBg)
  644. return warn("Couldn't find import menu background element");
  645. menuBg.style.visibility = "hidden";
  646. menuBg.style.display = "none";
  647. }
  648. /** Opens the import menu if it is closed */
  649. function openImportMenu() {
  650. if(isImportMenuOpen)
  651. return;
  652. isImportMenuOpen = true;
  653. document.body.classList.add("bytm-disable-scroll");
  654. const menuBg = document.querySelector<HTMLElement>("#bytm-import-menu-bg");
  655. if(!menuBg)
  656. return warn("Couldn't find import menu background element");
  657. menuBg.style.visibility = "visible";
  658. menuBg.style.display = "block";
  659. }
  660. //#MARKER changelog menu
  661. let isChangelogMenuOpen = false;
  662. /** Adds a changelog menu (hidden by default) */
  663. async function addChangelogMenu() {
  664. const menuBgElem = document.createElement("div");
  665. menuBgElem.id = "bytm-changelog-menu-bg";
  666. menuBgElem.classList.add("bytm-menu-bg");
  667. menuBgElem.title = "Click here to close the menu";
  668. menuBgElem.style.visibility = "hidden";
  669. menuBgElem.style.display = "none";
  670. menuBgElem.addEventListener("click", (e) => {
  671. if(isChangelogMenuOpen && (e.target as HTMLElement)?.id === "bytm-changelog-menu-bg") {
  672. closeChangelogMenu(e);
  673. openMenu();
  674. }
  675. });
  676. document.body.addEventListener("keydown", (e) => {
  677. if(isChangelogMenuOpen && e.key === "Escape") {
  678. closeChangelogMenu(e);
  679. openMenu();
  680. }
  681. });
  682. const menuContainer = document.createElement("div");
  683. menuContainer.title = ""; // prevent bg title from propagating downwards
  684. menuContainer.classList.add("bytm-menu");
  685. menuContainer.id = "bytm-changelog-menu";
  686. //#SECTION title bar
  687. const headerElem = document.createElement("div");
  688. headerElem.classList.add("bytm-menu-header");
  689. const titleCont = document.createElement("div");
  690. titleCont.id = "bytm-menu-titlecont";
  691. titleCont.role = "heading";
  692. titleCont.ariaLevel = "1";
  693. const titleElem = document.createElement("h2");
  694. titleElem.id = "bytm-menu-title";
  695. titleElem.innerText = `${scriptInfo.name} - Changelog`;
  696. const closeElem = document.createElement("img");
  697. closeElem.classList.add("bytm-menu-close");
  698. closeElem.src = await getResourceUrl("close");
  699. closeElem.title = "Click to close the menu";
  700. closeElem.addEventListener("click", (e) => {
  701. closeChangelogMenu(e);
  702. openMenu();
  703. });
  704. titleCont.appendChild(titleElem);
  705. headerElem.appendChild(titleCont);
  706. headerElem.appendChild(closeElem);
  707. //#SECTION body
  708. const menuBodyElem = document.createElement("div");
  709. menuBodyElem.id = "bytm-changelog-menu-body";
  710. menuBodyElem.classList.add("bytm-menu-body");
  711. const textElem = document.createElement("div");
  712. textElem.id = "bytm-changelog-menu-text";
  713. textElem.classList.add("bytm-markdown-container");
  714. textElem.innerHTML = changelogContent;
  715. //#SECTION finalize
  716. menuBodyElem.appendChild(textElem);
  717. menuContainer.appendChild(headerElem);
  718. menuContainer.appendChild(menuBodyElem);
  719. menuBgElem.appendChild(menuContainer);
  720. document.body.appendChild(menuBgElem);
  721. const anchors = document.querySelectorAll<HTMLAnchorElement>("#bytm-changelog-menu-text a");
  722. for(const anchor of anchors)
  723. anchor.target = "_blank";
  724. }
  725. /** Closes the changelog menu if it is open. If a bubbling event is passed, its propagation will be prevented. */
  726. function closeChangelogMenu(evt?: MouseEvent | KeyboardEvent) {
  727. if(!isChangelogMenuOpen)
  728. return;
  729. isChangelogMenuOpen = false;
  730. evt?.bubbles && evt.stopPropagation();
  731. document.body.classList.remove("bytm-disable-scroll");
  732. const menuBg = document.querySelector<HTMLElement>("#bytm-changelog-menu-bg");
  733. if(!menuBg)
  734. return warn("Couldn't find changelog menu background element");
  735. menuBg.style.visibility = "hidden";
  736. menuBg.style.display = "none";
  737. }
  738. /** Opens the changelog menu if it is closed */
  739. function openChangelogMenu() {
  740. if(isChangelogMenuOpen)
  741. return;
  742. isChangelogMenuOpen = true;
  743. document.body.classList.add("bytm-disable-scroll");
  744. const menuBg = document.querySelector<HTMLElement>("#bytm-changelog-menu-bg");
  745. if(!menuBg)
  746. return warn("Couldn't find changelog menu background element");
  747. menuBg.style.visibility = "visible";
  748. menuBg.style.display = "block";
  749. }