Github 时间格式转换

将 GitHub 页面上的相对时间转换为绝对日期和时间

  1. // ==UserScript==
  2. // @name Github Time Format Converter
  3. // @name:zh-CN Github 时间格式转换
  4. // @name:zh-TW Github 時間格式轉換
  5. // @description Convert relative times on GitHub to absolute date and time
  6. // @description:zh-CN 将 GitHub 页面上的相对时间转换为绝对日期和时间
  7. // @description:zh-TW 將 GitHub 頁面上的相對時間轉換成絕對日期與時間
  8. // @version 1.2.0
  9. // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/main/Icons/Github-Time-Format-Converter-Icon.svg
  10. // @author 念柚
  11. // @namespace https://github.com/MiPoNianYou/UserScripts
  12. // @supportURL https://github.com/MiPoNianYou/UserScripts/issues
  13. // @license GPL-3.0
  14. // @match https://github.com/*
  15. // @exclude https://github.com/topics/*
  16. // @grant GM_addStyle
  17. // @run-at document-idle
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. "use strict";
  22.  
  23. const Config = {
  24. SCRIPT_SETTINGS: {
  25. TOOLTIP_VERTICAL_OFFSET: 5,
  26. VIEWPORT_EDGE_MARGIN: 5,
  27. TRANSITION_DURATION_MS: 100,
  28. UI_FONT_STACK: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
  29. UI_FONT_STACK_MONO: "ui-monospace, SFMono-Regular, Menlo, monospace",
  30. },
  31. ELEMENT_IDS: {
  32. TOOLTIP_CONTAINER: "TimeConverterTooltipContainer",
  33. },
  34. CSS_CLASSES: {
  35. PROCESSED_TIME_ELEMENT: "time-converter-processed-element",
  36. TOOLTIP_IS_VISIBLE: "time-converter-tooltip--is-visible",
  37. },
  38. UI_TEXTS: {
  39. "zh-CN": {
  40. INVALID_DATE_STRING: "无效日期",
  41. },
  42. "zh-TW": {
  43. INVALID_DATE_STRING: "無效日期",
  44. },
  45. "en-US": {
  46. INVALID_DATE_STRING: "Invalid Date",
  47. },
  48. },
  49. DOM_SELECTORS: {
  50. RELATIVE_TIME: `relative-time:not(.${"time-converter-processed-element"})`,
  51. PROCESSED_TIME_SPAN: `span.${"time-converter-processed-element"}[data-tooltip-time]`,
  52. },
  53. };
  54. Config.DOM_SELECTORS.RELATIVE_TIME = `relative-time:not(.${Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT})`;
  55. Config.DOM_SELECTORS.PROCESSED_TIME_SPAN = `span.${Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT}[data-tooltip-time]`;
  56.  
  57. const State = {
  58. currentUserLocale: "en-US",
  59. localizedText: Config.UI_TEXTS["en-US"],
  60. shortDateFormatter: null,
  61. tooltipTimeFormatter: null,
  62.  
  63. initialize() {
  64. this.currentUserLocale = this.detectBrowserLanguage();
  65. this.localizedText =
  66. Config.UI_TEXTS[this.currentUserLocale] ||
  67. Config.UI_TEXTS["en-US"] ||
  68. {};
  69. this.initializeDateTimeFormatters(this.currentUserLocale);
  70. },
  71.  
  72. detectBrowserLanguage() {
  73. const languages = navigator.languages || [navigator.language];
  74. for (const lang of languages) {
  75. const langLower = lang.toLowerCase();
  76. if (langLower === "zh-cn") return "zh-CN";
  77. if (
  78. langLower === "zh-tw" ||
  79. langLower === "zh-hk" ||
  80. langLower === "zh-mo" ||
  81. langLower === "zh-hant"
  82. )
  83. return "zh-TW";
  84. if (langLower === "en-us") return "en-US";
  85. if (langLower.startsWith("zh-")) return "zh-CN";
  86. if (langLower.startsWith("en-")) return "en-US";
  87. }
  88. for (const lang of languages) {
  89. const langLower = lang.toLowerCase();
  90. if (langLower.startsWith("zh")) return "zh-CN";
  91. if (langLower.startsWith("en")) return "en-US";
  92. }
  93. return "en-US";
  94. },
  95.  
  96. initializeDateTimeFormatters(locale) {
  97. try {
  98. this.shortDateFormatter = new Intl.DateTimeFormat(locale, {
  99. year: "2-digit",
  100. month: "2-digit",
  101. day: "2-digit",
  102. });
  103.  
  104. this.tooltipTimeFormatter = new Intl.DateTimeFormat(locale, {
  105. hour: "2-digit",
  106. minute: "2-digit",
  107. second: "2-digit",
  108. hour12: false,
  109. });
  110. } catch (e) {
  111. this.shortDateFormatter = null;
  112. this.tooltipTimeFormatter = null;
  113. }
  114. },
  115.  
  116. getLocalizedText(key) {
  117. return (
  118. this.localizedText[key] ||
  119. (Config.UI_TEXTS["en-US"]
  120. ? Config.UI_TEXTS["en-US"][key]
  121. : undefined) ||
  122. key.replace(/_/g, " ")
  123. );
  124. },
  125. };
  126.  
  127. const UserInterface = {
  128. tooltipContainerElement: null,
  129.  
  130. injectStyles() {
  131. const appleEaseOutStandard = "cubic-bezier(0, 0, 0.58, 1)";
  132. const transitionDuration = Config.SCRIPT_SETTINGS.TRANSITION_DURATION_MS;
  133.  
  134. const cssStyles = `
  135. :root {
  136. --ctp-frappe-rosewater: rgb(242, 213, 207);
  137. --ctp-frappe-flamingo: rgb(238, 190, 190);
  138. --ctp-frappe-pink: rgb(244, 184, 228);
  139. --ctp-frappe-mauve: rgb(202, 158, 230);
  140. --ctp-frappe-red: rgb(231, 130, 132);
  141. --ctp-frappe-maroon: rgb(234, 153, 156);
  142. --ctp-frappe-peach: rgb(239, 159, 118);
  143. --ctp-frappe-yellow: rgb(229, 200, 144);
  144. --ctp-frappe-green: rgb(166, 209, 137);
  145. --ctp-frappe-teal: rgb(129, 200, 190);
  146. --ctp-frappe-sky: rgb(153, 209, 219);
  147. --ctp-frappe-sapphire: rgb(133, 193, 220);
  148. --ctp-frappe-blue: rgb(140, 170, 238);
  149. --ctp-frappe-lavender: rgb(186, 187, 241);
  150. --ctp-frappe-text: rgb(198, 208, 245);
  151. --ctp-frappe-subtext1: rgb(181, 191, 226);
  152. --ctp-frappe-subtext0: rgb(165, 173, 206);
  153. --ctp-frappe-overlay2: rgb(148, 156, 187);
  154. --ctp-frappe-overlay1: rgb(131, 139, 167);
  155. --ctp-frappe-overlay0: rgb(115, 121, 148);
  156. --ctp-frappe-surface2: rgb(98, 104, 128);
  157. --ctp-frappe-surface1: rgb(81, 87, 109);
  158. --ctp-frappe-surface0: rgb(65, 69, 89);
  159. --ctp-frappe-base: rgb(48, 52, 70);
  160. --ctp-frappe-mantle: rgb(41, 44, 60);
  161. --ctp-frappe-crust: rgb(35, 38, 52);
  162.  
  163. --ctp-latte-rosewater: rgb(220, 138, 120);
  164. --ctp-latte-flamingo: rgb(221, 120, 120);
  165. --ctp-latte-pink: rgb(234, 118, 203);
  166. --ctp-latte-mauve: rgb(136, 57, 239);
  167. --ctp-latte-red: rgb(210, 15, 57);
  168. --ctp-latte-maroon: rgb(230, 69, 83);
  169. --ctp-latte-peach: rgb(254, 100, 11);
  170. --ctp-latte-yellow: rgb(223, 142, 29);
  171. --ctp-latte-green: rgb(64, 160, 43);
  172. --ctp-latte-teal: rgb(23, 146, 153);
  173. --ctp-latte-sky: rgb(4, 165, 229);
  174. --ctp-latte-sapphire: rgb(32, 159, 181);
  175. --ctp-latte-blue: rgb(30, 102, 245);
  176. --ctp-latte-lavender: rgb(114, 135, 253);
  177. --ctp-latte-text: rgb(76, 79, 105);
  178. --ctp-latte-subtext1: rgb(92, 95, 119);
  179. --ctp-latte-subtext0: rgb(108, 111, 133);
  180. --ctp-latte-overlay2: rgb(124, 127, 147);
  181. --ctp-latte-overlay1: rgb(140, 143, 161);
  182. --ctp-latte-overlay0: rgb(156, 160, 176);
  183. --ctp-latte-surface2: rgb(172, 176, 190);
  184. --ctp-latte-surface1: rgb(188, 192, 204);
  185. --ctp-latte-surface0: rgb(204, 208, 218);
  186. --ctp-latte-base: rgb(239, 241, 245);
  187. --ctp-latte-mantle: rgb(230, 233, 239);
  188. --ctp-latte-crust: rgb(220, 224, 232);
  189.  
  190. --ctp-tooltip-bg-dark: rgb(from var(--ctp-frappe-mantle) r g b / 0.92);
  191. --ctp-tooltip-text-dark: var(--ctp-frappe-text);
  192. --ctp-tooltip-border-dark: rgb(from var(--ctp-frappe-surface2) r g b / 0.25);
  193. --ctp-tooltip-shadow-dark: 0 1px 3px rgba(0, 0, 0, 0.15), 0 5px 10px rgba(0, 0, 0, 0.2);
  194.  
  195. --ctp-tooltip-bg-light: rgb(from var(--ctp-latte-mantle) r g b / 0.92);
  196. --ctp-tooltip-text-light: var(--ctp-latte-text);
  197. --ctp-tooltip-border-light: rgb(from var(--ctp-latte-surface2) r g b / 0.3);
  198. --ctp-tooltip-shadow-light: 0 1px 3px rgba(90, 90, 90, 0.08), 0 5px 10px rgba(90, 90, 90, 0.12);
  199. }
  200.  
  201. #${Config.ELEMENT_IDS.TOOLTIP_CONTAINER} {
  202. position: fixed;
  203. padding: 6px 10px;
  204. border-radius: 8px;
  205. font-size: 12px;
  206. line-height: 1.4;
  207. z-index: 2147483647;
  208. pointer-events: none;
  209. white-space: pre;
  210. max-width: 350px;
  211. opacity: 0;
  212. visibility: hidden;
  213. font-family: ${Config.SCRIPT_SETTINGS.UI_FONT_STACK_MONO};
  214. backdrop-filter: blur(10px) saturate(180%);
  215. -webkit-backdrop-filter: blur(10px) saturate(180%);
  216. transition: opacity ${transitionDuration}ms ${appleEaseOutStandard},
  217. visibility ${transitionDuration}ms ${appleEaseOutStandard};
  218.  
  219. background-color: var(--ctp-tooltip-bg-dark);
  220. color: var(--ctp-tooltip-text-dark);
  221. border: 1px solid var(--ctp-tooltip-border-dark);
  222. box-shadow: var(--ctp-tooltip-shadow-dark);
  223. }
  224.  
  225. #${Config.ELEMENT_IDS.TOOLTIP_CONTAINER}.${Config.CSS_CLASSES.TOOLTIP_IS_VISIBLE} {
  226. opacity: 1;
  227. visibility: visible;
  228. }
  229.  
  230. .${Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT}[data-tooltip-time] {
  231. display: inline-block;
  232. vertical-align: baseline;
  233. font-family: ${Config.SCRIPT_SETTINGS.UI_FONT_STACK_MONO};
  234. text-align: right;
  235. margin: 0;
  236. padding: 0;
  237. box-sizing: border-box;
  238. cursor: help;
  239. color: inherit;
  240. background: none;
  241. border: none;
  242. }
  243.  
  244. @media (prefers-color-scheme: light) {
  245. #${Config.ELEMENT_IDS.TOOLTIP_CONTAINER} {
  246. background-color: var(--ctp-tooltip-bg-light);
  247. color: var(--ctp-tooltip-text-light);
  248. border: 1px solid var(--ctp-tooltip-border-light);
  249. box-shadow: var(--ctp-tooltip-shadow-light);
  250. }
  251. }
  252. `;
  253. try {
  254. GM_addStyle(cssStyles);
  255. } catch (e) {}
  256. },
  257.  
  258. ensureTooltipContainer() {
  259. this.tooltipContainerElement = document.getElementById(
  260. Config.ELEMENT_IDS.TOOLTIP_CONTAINER
  261. );
  262. if (!this.tooltipContainerElement && document.body) {
  263. this.tooltipContainerElement = document.createElement("div");
  264. this.tooltipContainerElement.id = Config.ELEMENT_IDS.TOOLTIP_CONTAINER;
  265. this.tooltipContainerElement.setAttribute("role", "tooltip");
  266. this.tooltipContainerElement.setAttribute("aria-hidden", "true");
  267. try {
  268. document.body.appendChild(this.tooltipContainerElement);
  269. } catch (e) {}
  270. }
  271. return this.tooltipContainerElement;
  272. },
  273.  
  274. displayTooltip(targetElement) {
  275. const tooltipTime = targetElement.dataset.tooltipTime;
  276. this.ensureTooltipContainer();
  277.  
  278. if (!tooltipTime || !this.tooltipContainerElement) return;
  279.  
  280. this.tooltipContainerElement.textContent = tooltipTime;
  281. this.tooltipContainerElement.setAttribute("aria-hidden", "false");
  282.  
  283. const targetRect = targetElement.getBoundingClientRect();
  284. this.tooltipContainerElement.classList.add(
  285. Config.CSS_CLASSES.TOOLTIP_IS_VISIBLE
  286. );
  287.  
  288. this.tooltipContainerElement.style.left = "-9999px";
  289. this.tooltipContainerElement.style.top = "-9999px";
  290. this.tooltipContainerElement.style.visibility = "hidden";
  291.  
  292. requestAnimationFrame(() => {
  293. if (!this.tooltipContainerElement || !targetElement.isConnected) {
  294. this.hideTooltip();
  295. return;
  296. }
  297.  
  298. const tooltipWidth = this.tooltipContainerElement.offsetWidth;
  299. const tooltipHeight = this.tooltipContainerElement.offsetHeight;
  300. const viewportWidth = window.innerWidth;
  301. const viewportHeight = window.innerHeight;
  302. const verticalOffset = Config.SCRIPT_SETTINGS.TOOLTIP_VERTICAL_OFFSET;
  303. const margin = Config.SCRIPT_SETTINGS.VIEWPORT_EDGE_MARGIN;
  304.  
  305. let tooltipLeft =
  306. targetRect.left + targetRect.width / 2 - tooltipWidth / 2;
  307. tooltipLeft = Math.max(margin, tooltipLeft);
  308. tooltipLeft = Math.min(
  309. viewportWidth - tooltipWidth - margin,
  310. tooltipLeft
  311. );
  312.  
  313. let tooltipTop;
  314. const spaceAbove = targetRect.top - verticalOffset;
  315. const spaceBelow = viewportHeight - targetRect.bottom - verticalOffset;
  316.  
  317. if (spaceAbove >= tooltipHeight + margin) {
  318. tooltipTop = targetRect.top - tooltipHeight - verticalOffset;
  319. } else if (spaceBelow >= tooltipHeight + margin) {
  320. tooltipTop = targetRect.bottom + verticalOffset;
  321. } else {
  322. tooltipTop =
  323. spaceAbove > spaceBelow
  324. ? Math.max(
  325. margin,
  326. targetRect.top - tooltipHeight - verticalOffset
  327. )
  328. : Math.min(
  329. viewportHeight - tooltipHeight - margin,
  330. targetRect.bottom + verticalOffset
  331. );
  332. if (tooltipTop < margin) tooltipTop = margin;
  333. }
  334.  
  335. this.tooltipContainerElement.style.left = `${tooltipLeft}px`;
  336. this.tooltipContainerElement.style.top = `${tooltipTop}px`;
  337. this.tooltipContainerElement.style.visibility = "visible";
  338. });
  339. },
  340.  
  341. hideTooltip() {
  342. if (this.tooltipContainerElement) {
  343. this.tooltipContainerElement.classList.remove(
  344. Config.CSS_CLASSES.TOOLTIP_IS_VISIBLE
  345. );
  346. this.tooltipContainerElement.setAttribute("aria-hidden", "true");
  347. }
  348. },
  349. };
  350.  
  351. const TimeConverter = {
  352. formatDateTime(dateSource, formatType) {
  353. const dateObject =
  354. typeof dateSource === "string" ? new Date(dateSource) : dateSource;
  355. if (isNaN(dateObject.getTime())) {
  356. return State.getLocalizedText("INVALID_DATE_STRING");
  357. }
  358.  
  359. let formatter;
  360. if (formatType === "shortDate") {
  361. formatter = State.shortDateFormatter;
  362. } else if (formatType === "tooltipTime") {
  363. formatter = State.tooltipTimeFormatter;
  364. } else {
  365. formatter = State.shortDateFormatter;
  366. }
  367.  
  368. if (!formatter) {
  369. const year = String(dateObject.getFullYear()).slice(-2);
  370. const month = String(dateObject.getMonth() + 1).padStart(2, "0");
  371. const day = String(dateObject.getDate()).padStart(2, "0");
  372. const hours = String(dateObject.getHours()).padStart(2, "0");
  373. const minutes = String(dateObject.getMinutes()).padStart(2, "0");
  374. const seconds = String(dateObject.getSeconds()).padStart(2, "0");
  375.  
  376. if (formatType === "shortDate") {
  377. return `${year}-${month}-${day}`;
  378. } else if (formatType === "tooltipTime") {
  379. return `${hours}:${minutes}:${seconds}`;
  380. }
  381. return `${year}-${month}-${day}`;
  382. }
  383.  
  384. try {
  385. let formattedString = formatter.format(dateObject);
  386. formattedString = formattedString.replace(/[/]/g, "-");
  387. if (formatType !== "shortDate" && formatType !== "tooltipTime") {
  388. formattedString = formattedString.replace(", ", " ");
  389. }
  390. return formattedString;
  391. } catch (e) {
  392. return State.getLocalizedText("INVALID_DATE_STRING");
  393. }
  394. },
  395.  
  396. convertElement(element) {
  397. if (
  398. !element ||
  399. !(element instanceof Element) ||
  400. element.classList.contains(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT)
  401. ) {
  402. return;
  403. }
  404.  
  405. const dateTimeAttribute = element.getAttribute("datetime");
  406. if (!dateTimeAttribute) {
  407. element.classList.add(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT);
  408. return;
  409. }
  410.  
  411. try {
  412. const displayDateText = this.formatDateTime(
  413. dateTimeAttribute,
  414. "shortDate"
  415. );
  416. const tooltipTimeText = this.formatDateTime(
  417. dateTimeAttribute,
  418. "tooltipTime"
  419. );
  420.  
  421. if (
  422. displayDateText === State.getLocalizedText("INVALID_DATE_STRING") ||
  423. tooltipTimeText === State.getLocalizedText("INVALID_DATE_STRING")
  424. ) {
  425. element.classList.add(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT);
  426. return;
  427. }
  428.  
  429. const replacementSpan = document.createElement("span");
  430. replacementSpan.textContent = displayDateText;
  431. replacementSpan.dataset.tooltipTime = tooltipTimeText;
  432. replacementSpan.classList.add(
  433. Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT
  434. );
  435.  
  436. if (element.parentNode) {
  437. element.parentNode.replaceChild(replacementSpan, element);
  438. } else {
  439. element.classList.add(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT);
  440. }
  441. } catch (error) {
  442. element.classList.add(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT);
  443. }
  444. },
  445.  
  446. processAllInNode(targetNode = document.body) {
  447. if (!targetNode || typeof targetNode.querySelectorAll !== "function")
  448. return;
  449. try {
  450. const timeElements = targetNode.querySelectorAll(
  451. Config.DOM_SELECTORS.RELATIVE_TIME
  452. );
  453. timeElements.forEach(this.convertElement.bind(this));
  454. } catch (e) {}
  455. },
  456. };
  457.  
  458. const EventManager = {
  459. domMutationObserver: null,
  460.  
  461. init() {
  462. this.setupTooltipListeners();
  463. this.startDomObserver();
  464. },
  465.  
  466. setupTooltipListeners() {
  467. document.body.addEventListener("mouseover", (event) => {
  468. const targetSpan = event.target.closest(
  469. Config.DOM_SELECTORS.PROCESSED_TIME_SPAN
  470. );
  471. if (targetSpan) UserInterface.displayTooltip(targetSpan);
  472. });
  473. document.body.addEventListener("mouseout", (event) => {
  474. const targetSpan = event.target.closest(
  475. Config.DOM_SELECTORS.PROCESSED_TIME_SPAN
  476. );
  477. if (
  478. targetSpan &&
  479. (!event.relatedTarget ||
  480. !UserInterface.tooltipContainerElement?.contains(
  481. event.relatedTarget
  482. ))
  483. ) {
  484. UserInterface.hideTooltip();
  485. }
  486. });
  487. document.body.addEventListener(
  488. "focusin",
  489. (event) => {
  490. const targetSpan = event.target.closest(
  491. Config.DOM_SELECTORS.PROCESSED_TIME_SPAN
  492. );
  493. if (targetSpan) UserInterface.displayTooltip(targetSpan);
  494. },
  495. true
  496. );
  497. document.body.addEventListener(
  498. "focusout",
  499. (event) => {
  500. const targetSpan = event.target.closest(
  501. Config.DOM_SELECTORS.PROCESSED_TIME_SPAN
  502. );
  503. if (targetSpan) UserInterface.hideTooltip();
  504. },
  505. true
  506. );
  507. },
  508.  
  509. handleDomMutation(mutations) {
  510. for (const mutation of mutations) {
  511. if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
  512. for (const addedNode of mutation.addedNodes) {
  513. if (addedNode.nodeType === Node.ELEMENT_NODE) {
  514. if (addedNode.matches(Config.DOM_SELECTORS.RELATIVE_TIME)) {
  515. TimeConverter.convertElement(addedNode);
  516. } else if (
  517. addedNode.querySelector(Config.DOM_SELECTORS.RELATIVE_TIME)
  518. ) {
  519. const descendantElements = addedNode.querySelectorAll(
  520. Config.DOM_SELECTORS.RELATIVE_TIME
  521. );
  522. descendantElements.forEach(
  523. TimeConverter.convertElement.bind(TimeConverter)
  524. );
  525. }
  526. }
  527. }
  528. }
  529. }
  530. },
  531.  
  532. startDomObserver() {
  533. if (this.domMutationObserver) return;
  534. this.domMutationObserver = new MutationObserver(
  535. this.handleDomMutation.bind(this)
  536. );
  537. const observerConfiguration = { childList: true, subtree: true };
  538.  
  539. if (document.body) {
  540. try {
  541. this.domMutationObserver.observe(
  542. document.body,
  543. observerConfiguration
  544. );
  545. } catch (e) {}
  546. } else {
  547. document.addEventListener(
  548. "DOMContentLoaded",
  549. () => {
  550. if (document.body) {
  551. try {
  552. this.domMutationObserver.observe(
  553. document.body,
  554. observerConfiguration
  555. );
  556. } catch (e) {}
  557. }
  558. },
  559. { once: true }
  560. );
  561. }
  562. },
  563. };
  564.  
  565. const ScriptManager = {
  566. init() {
  567. try {
  568. State.initialize();
  569. UserInterface.injectStyles();
  570. UserInterface.ensureTooltipContainer();
  571. TimeConverter.processAllInNode(
  572. document.body || document.documentElement
  573. );
  574. EventManager.init();
  575. } catch (error) {}
  576. },
  577. };
  578.  
  579. if (
  580. document.readyState === "complete" ||
  581. (document.readyState !== "loading" && !document.documentElement.doScroll)
  582. ) {
  583. ScriptManager.init();
  584. } else {
  585. document.addEventListener("DOMContentLoaded", () => ScriptManager.init(), {
  586. once: true,
  587. });
  588. }
  589. })();