Greasy Fork is available in English.

GitHub Toggle Issue Comments

A userscript that toggles issues/pull request comments & messages

Version vom 09.04.2018. Aktuellste Version

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  1. // ==UserScript==
  2. // @name GitHub Toggle Issue Comments
  3. // @version 1.1.5
  4. // @description A userscript that toggles issues/pull request comments & messages
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @include https://github.com/*
  9. // @run-at document-idle
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @require https://greatest.deepsurf.us/scripts/28721-mutations/code/mutations.js?version=264157
  14. // @icon https://assets-cdn.github.com/pinned-octocat.svg
  15. // ==/UserScript==
  16. (() => {
  17. "use strict";
  18.  
  19. GM_addStyle(`
  20. .ghic-button { float:right; }
  21. .ghic-button .btn:hover div.select-menu-modal-holder { display:block; top:auto; bottom:25px; right:0; }
  22. .ghic-right { float:right; }
  23. /* pre-wrap set for Firefox; see https://greatest.deepsurf.us/en/forum/discussion/9166/x */
  24. .ghic-menu label { display:block; padding:5px 15px; white-space:pre-wrap; }
  25. .ghic-button .select-menu-header, .ghic-participants { cursor:default; }
  26. .ghic-participants { border-top:1px solid #484848; padding:15px; }
  27. .ghic-avatar { display:inline-block; float:left; margin: 0 2px 2px 0; cursor:pointer; position:relative; }
  28. .ghic-avatar:last-child { margin-bottom:5px; }
  29. .ghic-avatar.comments-hidden svg { display:block; position:absolute; top:-2px; left:-2px; z-index:1; }
  30. .ghic-avatar.comments-hidden img { opacity:0.5; }
  31. .ghic-button .dropdown-item span { font-weight:normal; opacity:.5; }
  32. .ghic-button .dropdown-item.ghic-has-content span { opacity:1; }
  33. .ghic-button .dropdown-item.ghic-checked span { font-weight:bold; }
  34. .ghic-button .dropdown-item.ghic-checked svg,
  35. .ghic-button .dropdown-item.ghic-checked .ghic-count { display:inline-block; }
  36. .ghic-button .ghic-count { float:left; margin-right:5px; }
  37. .ghic-button .select-menu-modal { margin:0; }
  38. .ghic-button .ghic-participants { margin-bottom:20px; }
  39. /* for testing: ".ghic-hidden { opacity: 0.3; } */
  40. .ghic-hidden, .ghic-hidden-participant, .ghic-avatar svg, .ghic-button .ghic-right > *,
  41. .ghic-hideReactions .comment-reactions { display:none; }
  42. `);
  43.  
  44. const regex = /(svg|path)/i,
  45. // ZenHub addon active (include ZenHub Enterprise)
  46. hasZenHub = $(".zhio, .zhe") ? true : false,
  47.  
  48. settings = {
  49. // example: https://github.com/Mottie/Keyboard/issues/448
  50. title: {
  51. isHidden: false,
  52. name: "ghic-title",
  53. selector: ".discussion-item-renamed",
  54. label: "Title Changes"
  55. },
  56. labels: {
  57. isHidden: false,
  58. name: "ghic-labels",
  59. selector: ".discussion-item-labeled, .discussion-item-unlabeled",
  60. label: "Label Changes"
  61. },
  62. state: {
  63. isHidden: false,
  64. name: "ghic-state",
  65. selector: ".discussion-item-reopened, .discussion-item-closed",
  66. label: "State Changes (close/reopen)"
  67. },
  68.  
  69. // example: https://github.com/jquery/jquery/issues/2986
  70. milestone: {
  71. isHidden: false,
  72. name: "ghic-milestone",
  73. selector: ".discussion-item-milestoned",
  74. label: "Milestone Changes"
  75. },
  76. refs: {
  77. isHidden: false,
  78. name: "ghic-refs",
  79. selector: ".discussion-item",
  80. contains: ".discussion-item-ref-title",
  81. label: "References"
  82. },
  83. assigned: {
  84. isHidden: false,
  85. name: "ghic-assigned",
  86. selector: ".discussion-item-assigned",
  87. label: "Assignment Changes"
  88. },
  89.  
  90. // Pull Requests
  91. commits: {
  92. isHidden: false,
  93. name: "ghic-commits",
  94. selector: ".discussion-commits",
  95. label: "Commits"
  96. },
  97. reviews: {
  98. isHidden: false,
  99. name: "ghic-reviews",
  100. selector: ".discussion-item-review, .discussion-item-review_requested",
  101. label: "Reviews (All)"
  102. },
  103. outdated: {
  104. isHidden: false,
  105. name: "ghic-outdated",
  106. selector: ".discussion-item-review",
  107. contains: ".outdated-comment-label",
  108. label: "Reviews (Outdated)"
  109. },
  110. // example: https://github.com/jquery/jquery/pull/3014
  111. diffOld: {
  112. isHidden: false,
  113. name: "ghic-diffOld",
  114. selector: ".outdated-diff-comment-container",
  115. label: "Diff (outdated) Comments"
  116. },
  117. diffNew: {
  118. isHidden: false,
  119. name: "ghic-diffNew",
  120. selector: "[id^=diff-for-comment-]:not(.outdated-diff-comment-container)",
  121. label: "Diff (current) Comments"
  122. },
  123. // example: https://github.com/jquery/jquery/pull/2949
  124. merged: {
  125. isHidden: false,
  126. name: "ghic-merged",
  127. selector: ".discussion-item-merged",
  128. label: "Merged"
  129. },
  130. integrate: {
  131. isHidden: false,
  132. name: "ghic-integrate",
  133. selector: ".discussion-item-integrations-callout",
  134. label: "Integrations"
  135. },
  136.  
  137. // extras (special treatment - no selector)
  138. plus1: {
  139. isHidden: false,
  140. name: "ghic-plus1",
  141. label: "Hide +1s"
  142. },
  143. reactions: {
  144. isHidden: false,
  145. name: "ghic-reactions",
  146. label: "Reactions"
  147. },
  148. // page with lots of users to hide:
  149. // https://github.com/isaacs/github/issues/215
  150.  
  151. // ZenHub pipeline change
  152. pipeline: {
  153. isHidden: false,
  154. name: "ghic-pipeline",
  155. selector: ".discussion-item.zh-discussion-item",
  156. label: "ZenHub Pipeline Changes"
  157. }
  158. };
  159.  
  160. const iconHidden = `<svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 9 9"><path fill="#777" d="M7.07 4.5c0-.47-.12-.9-.35-1.3L3.2 6.7c.4.25.84.37 1.3.37.35 0 .68-.07 1-.2.32-.14.6-.32.82-.55.23-.23.4-.5.55-.82.13-.32.2-.65.2-1zM2.3 5.8l3.5-3.52c-.4-.23-.83-.35-1.3-.35-.35 0-.68.07-1 .2-.3.14-.6.32-.82.55-.23.23-.4.5-.55.82-.13.32-.2.65-.2 1 0 .47.12.9.36 1.3zm6.06-1.3c0 .7-.17 1.34-.52 1.94-.34.6-.8 1.05-1.4 1.4-.6.34-1.24.52-1.94.52s-1.34-.18-1.94-.52c-.6-.35-1.05-.8-1.4-1.4C.82 5.84.64 5.2.64 4.5s.18-1.35.52-1.94.8-1.06 1.4-1.4S3.8.64 4.5.64s1.35.17 1.94.52 1.06.8 1.4 1.4c.35.6.52 1.24.52 1.94z"/></svg>`,
  161. iconCheck = `<svg class="octicon octicon-check" height="16" viewBox="0 0 12 16" width="12"><path d="M12 5L4 13 0 9l1.5-1.5 2.5 2.5 6.5-6.5 1.5 1.5z"></path></svg>`,
  162. plus1Icon = `<img src="https://assets-cdn.github.com/images/icons/emoji/unicode/1f44d.png" class="emoji" title=":+1:" alt=":+1:" height="20" width="20" align="absmiddle">`;
  163.  
  164. function addMenu() {
  165. if ($("#discussion_bucket") && !$(".ghic-button")) {
  166. // update "isHidden" values
  167. getSettings();
  168. let name, bright, isHidden, isChecked,
  169. list = "",
  170. keys = Object.keys(settings),
  171. header = $(".discussion-sidebar-item:last-child"),
  172. menu = document.createElement("div");
  173.  
  174. for (name of keys) {
  175. if (!(name === "pipeline" && !hasZenHub)) {
  176. // make plus1 and reactions list items always bright
  177. bright = name === "plus1" ? " ghic-has-content" : "";
  178. isHidden = settings[name].isHidden;
  179. isChecked = isHidden ? " ghic-checked": "";
  180. // not using multi-line backticks because it adds lots of white-space to the label
  181. list += `<label class="dropdown-item${bright}${isChecked}">` +
  182. `<span>${settings[name].label}</span>` +
  183. `<span class="ghic-right ${settings[name].name}">` +
  184. `<input type="checkbox"${isHidden ? " checked" : ""}>` +
  185. `${iconCheck}<span class="ghic-count"> </span>` +
  186. `</span></label>`;
  187. }
  188. }
  189.  
  190. menu.className = "ghic-button";
  191. menu.innerHTML = `
  192. <span class="btn btn-sm" role="button" tabindex="0" aria-haspopup="true">
  193. <span class="tooltipped tooltipped-w" aria-label="Toggle issue comments">
  194. <svg class="octicon octicon-comment-discussion" height="16" width="16" role="img" viewBox="0 0 16 16">
  195. <path d="M15 2H6c-0.55 0-1 0.45-1 1v2H1c-0.55 0-1 0.45-1 1v6c0 0.55 0.45 1 1 1h1v3l3-3h4c0.55 0 1-0.45 1-1V10h1l3 3V10h1c0.55 0 1-0.45 1-1V3c0-0.55-0.45-1-1-1zM9 12H4.5l-1.5 1.5v-1.5H1V6h4v3c0 0.55 0.45 1 1 1h3v2z m6-3H13v1.5l-1.5-1.5H6V3h9v6z"></path>
  196. </svg>
  197. </span>
  198. <div class="select-menu-modal-holder">
  199. <div class="select-menu-modal" aria-hidden="true">
  200. <div class="select-menu-header" tabindex="-1">
  201. <span class="select-menu-title">Toggle items</span>
  202. </div>
  203. <div class="select-menu-list ghic-menu" role="menu">
  204. ${list}
  205. <div class="ghic-participants"></div>
  206. </div>
  207. </div>
  208. </div>
  209. </span>
  210. `;
  211. if (hasZenHub) {
  212. header.insertBefore(menu, header.childNodes[0]);
  213. } else {
  214. header.appendChild(menu);
  215. }
  216. addAvatars();
  217. }
  218. update();
  219. }
  220.  
  221. function addAvatars() {
  222. let indx = 0,
  223.  
  224. str = "<p><strong>Hide Comments from</strong></p>",
  225. unique = [],
  226. // get all avatars
  227. avatars = $$(".timeline-comment-avatar img"),
  228. len = avatars.length - 1, // last avatar is the new comment with the current user
  229.  
  230. loop = (callback) => {
  231. let el, name,
  232. max = 0;
  233. while (max < 50 && indx < len) {
  234. if (indx >= len) {
  235. return callback();
  236. }
  237. el = avatars[indx];
  238. name = (el.getAttribute("alt") || "").replace("@", "");
  239. if (!unique.includes(name)) {
  240. str += `<span class="ghic-avatar tooltipped tooltipped-n" aria-label="${name}">
  241. ${iconHidden}
  242. <img class="ghic-avatar avatar" width="24" height="24" src="${el.src}"/>
  243. </span>`;
  244. unique[unique.length] = name;
  245. max++;
  246. }
  247. indx++;
  248. }
  249. if (indx < len) {
  250. setTimeout(() => {
  251. loop(callback);
  252. }, 200);
  253. } else {
  254. callback();
  255. }
  256. };
  257. loop(() => {
  258. $(".ghic-participants").innerHTML = str;
  259. });
  260. }
  261.  
  262. function getSettings() {
  263. let name,
  264. keys = Object.keys(settings);
  265. for (name of keys) {
  266. settings[name].isHidden = GM_getValue(settings[name].name, false);
  267. }
  268. }
  269.  
  270. function saveSettings() {
  271. let name,
  272. keys = Object.keys(settings);
  273. for (name of keys) {
  274. GM_setValue(settings[name].name, settings[name].isHidden);
  275. }
  276. }
  277.  
  278. function getInputValues() {
  279. let name, item,
  280. keys = Object.keys(settings),
  281. menu = $(".ghic-menu");
  282. for (name of keys) {
  283. if (!(name === "pipeline" && !hasZenHub)) {
  284. item = closest(".dropdown-item", $("." + settings[name].name, menu));
  285. settings[name].isHidden = $("input", item).checked;
  286. toggleClass(item, "ghic-checked", settings[name].isHidden);
  287. }
  288. }
  289. }
  290.  
  291. function hideStuff(name, init) {
  292. const obj = settings[name],
  293. isHidden = obj.isHidden;
  294. let count, results,
  295. item = closest(".dropdown-item", $(".ghic-menu ." + obj.name));
  296. if (obj.selector) {
  297. results = $$(obj.selector);
  298. if (obj.contains) {
  299. results = results.filter(el => {
  300. return !!$(obj.contains, el);
  301. });
  302. }
  303. toggleClass(item, "ghic-checked", isHidden);
  304. if (isHidden) {
  305. count = addClass(results, "ghic-hidden");
  306. $(".ghic-count", item).textContent = count ? "(" + count + ")" : " ";
  307. } else if (!init) {
  308. // no need to remove classes on initialization
  309. removeClass(results, "ghic-hidden");
  310. }
  311. toggleClass(item, "ghic-has-content", results.length);
  312. } else if (name === "plus1") {
  313. hidePlus1(init);
  314. } else if (name === "reactions") {
  315. toggleClass($("body"), "ghic-hideReactions", isHidden);
  316. toggleClass(item, "ghic-has-content", $$(".has-reactions").length - 1);
  317. // make first comment reactions visible
  318. item = $(".has-reactions", $(".timeline-comment-wrapper"));
  319. if (item) {
  320. item.style.display = "block";
  321. }
  322. }
  323. }
  324.  
  325. function hidePlus1(init) {
  326. if (init && !settings.plus1.isHidden) {
  327. return;
  328. }
  329. let max,
  330. indx = 0,
  331. count = 0,
  332. total = 0,
  333. // keep a list of post authors to prevent duplicate +1 counts
  334. authors = [],
  335. // used https://github.com/isaacs/github/issues/215 for matches here...
  336. // matches "+1!!!!", "++1", "+!", "+99!!!", "-1", "+ 100", "thumbs up"; ":+1:^21425235"
  337. // ignoring -1's... add unicode for thumbs up; it gets replaced with an image in Windows
  338. regexPlus = /([?!,.:^[\]()\'\"+-\d]|bump|thumbs|up|\ud83d\udc4d)/gi,
  339. // other comments to hide - they are still counted towards the +1 counter (for now?)
  340. // seen "^^^" to bump posts; "bump plleeaaassee"; "eta?"; "pretty please"
  341. // "need this"; "right now"; "still nothing?"; "super helpful"; "for gods sake"
  342. regexHide = new RegExp("(" + [
  343. "@\\w+",
  344. "pretty",
  345. "pl+e+a+s+e+",
  346. "y+e+s+",
  347. "eta",
  348. "much",
  349. "need(ed)?",
  350. "fix",
  351. "this",
  352. "right",
  353. "now",
  354. "still",
  355. "nothing",
  356. "super",
  357. "helpful",
  358. "for\\sgods\\ssake",
  359. "c'?mon",
  360. "come\\son"
  361. ].join("|") + ")", "gi"),
  362. // image title ":{anything}:", etc.
  363. regexEmoji = /:(.*):/,
  364.  
  365. comments = $$(".js-discussion .timeline-comment-wrapper"),
  366. len = comments.length,
  367.  
  368. loop = () => {
  369. let wrapper, el, tmp, txt, img, hasLink, dupe;
  370. max = 0;
  371. while (max < 20 && indx < len) {
  372. if (indx >= len) {
  373. return;
  374. }
  375. wrapper = comments[indx];
  376. // save author list to prevent repeat +1s
  377. el = $(".timeline-comment-header .author", wrapper);
  378. txt = (el ? el.textContent || "" : "").toLowerCase();
  379. dupe = true;
  380. if (txt && authors.indexOf(txt) < 0) {
  381. authors[authors.length] = txt;
  382. dupe = false;
  383. }
  384. el = $(".comment-body", wrapper);
  385. // ignore quoted messages, but get all fragments
  386. tmp = $$(".email-fragment", el);
  387. // some posts only contain a link to related issues; these should not be counted as a +1
  388. // see https://github.com/isaacs/github/issues/618#issuecomment-200869630
  389. hasLink = $$(tmp.length ? ".email-fragment .issue-link" : ".issue-link", el).length;
  390. if (tmp.length) {
  391. // ignore quoted messages
  392. txt = getAllText(tmp);
  393. } else {
  394. txt = el.textContent.trim();
  395. }
  396. if (!txt) {
  397. img = $("img", el);
  398. if (img) {
  399. txt = img.getAttribute("title") || img.getAttribute("alt");
  400. }
  401. }
  402. // remove fluff
  403. txt = txt.replace(regexEmoji, "").replace(regexPlus, "").replace(regexHide, "").trim();
  404. if (txt === "" || (txt.length < 4 && !hasLink)) {
  405. if (settings.plus1.isHidden) {
  406. wrapper.classList.add("ghic-hidden");
  407. total++;
  408. // one +1 per author
  409. if (!dupe) {
  410. count++;
  411. }
  412. } else if (!init) {
  413. wrapper.classList.remove("ghic-hidden");
  414. }
  415. max++;
  416. }
  417. indx++;
  418. }
  419. if (indx < len) {
  420. setTimeout(() => {
  421. loop();
  422. }, 200);
  423. } else {
  424. $(".ghic-menu .ghic-plus1 .ghic-count").textContent = total ? "(" + total + ")" : " ";
  425. toggleClass($(".ghic-menu ." + settings.plus1.name), "ghic-has-content", total);
  426. addCountToReaction(count);
  427. }
  428. };
  429. loop();
  430. }
  431.  
  432. function getAllText(el) {
  433. let txt = "",
  434. indx = el.length;
  435. // text order doesn't matter
  436. while (indx--) {
  437. txt += el[indx].textContent.trim();
  438. }
  439. return txt;
  440. }
  441.  
  442. function addCountToReaction(count) {
  443. if (!count) {
  444. count = ($(".ghic-menu .ghic-plus1 .ghic-count").textContent || "")
  445. .replace(/[()]/g, "")
  446. .trim();
  447. }
  448. let comment = $(".timeline-comment"),
  449. tmp = $(
  450. ".has-reactions button[value='+1 react'], .has-reactions button[value='+1 unreact']",
  451. comment
  452. ),
  453. el = $(".ghic-count", comment);
  454. if (el) {
  455. // the count may have been appended to the comment & now
  456. // there is a reaction, so remove any "ghic-count" elements
  457. el.parentNode.removeChild(el);
  458. }
  459. if (count) {
  460. if (tmp) {
  461. el = document.createElement("span");
  462. el.className = "ghic-count";
  463. el.textContent = count ? " + " + count + " (from hidden comments)" : "";
  464. tmp.appendChild(el);
  465. } else {
  466. el = document.createElement("p");
  467. el.className = "ghic-count";
  468. el.innerHTML = "<hr>" + plus1Icon + " " + count + " (from hidden comments)";
  469. $(".comment-body", comment).appendChild(el);
  470. }
  471. }
  472. }
  473.  
  474. function hideParticipant(el) {
  475. if (el) {
  476. el.classList.toggle("comments-hidden");
  477. let name = el.getAttribute("aria-label"),
  478. results = $$(
  479. ".timeline-comment-wrapper, .commit-comment, .discussion-item"
  480. ).filter(el => {
  481. const author = $(".js-discussion .author", el);
  482. return author ? name === author.textContent.trim() : false;
  483. });
  484. // use a different participant class name to hide timeline events
  485. // or unselecting all users will show everything
  486. if (el.classList.contains("comments-hidden")) {
  487. addClass(results, "ghic-hidden-participant");
  488. } else {
  489. removeClass(results, "ghic-hidden-participant");
  490. }
  491. results = [];
  492. }
  493. }
  494.  
  495. function update() {
  496. if ($("#discussion_bucket") && $(".ghic-button")) {
  497. let keys = Object.keys(settings),
  498. indx = keys.length;
  499. while (indx--) {
  500. // true flag for init - no need to remove classes
  501. hideStuff(keys[indx], true);
  502. }
  503. }
  504. }
  505.  
  506. function checkItem(event) {
  507. if (document.getElementById("discussion_bucket")) {
  508. let name,
  509. target = event.target,
  510. wrap = target && target.parentNode;
  511. if (target && wrap) {
  512. if (target.nodeName === "INPUT" && wrap.classList.contains("ghic-right")) {
  513. getInputValues();
  514. saveSettings();
  515. // extract ghic-{name}, because it matches the name in settings
  516. name = wrap.className.replace("ghic-right", "").replace("ghic-has-content", "").trim();
  517. if (wrap.classList.contains(name)) {
  518. hideStuff(name.replace("ghic-", ""));
  519. }
  520. } else if (target.classList.contains("ghic-avatar")) {
  521. // make sure we're targeting the span wrapping the image
  522. hideParticipant(target.nodeName === "IMG" ? target.parentNode : target);
  523. } else if (regex.test(target.nodeName)) {
  524. // clicking on the SVG may target the svg or path inside
  525. hideParticipant(closest(".ghic-avatar", target));
  526. }
  527. }
  528. // Make button show if it is active
  529. target = $(".ghic-button .btn");
  530. if (target) {
  531. const active = $$(".ghic-hidden, .ghic-hidden-participant").length > 0;
  532. target.classList.toggle("btn-outline", active);
  533. }
  534. }
  535. }
  536.  
  537. function $(selector, el) {
  538. return (el || document).querySelector(selector);
  539. }
  540.  
  541. function $$(selector, el) {
  542. return Array.from((el || document).querySelectorAll(selector));
  543. }
  544.  
  545. function closest(selector, el) {
  546. while (el && el.nodeType === 1) {
  547. if (el.matches(selector)) {
  548. return el;
  549. }
  550. el = el.parentNode;
  551. }
  552. return null;
  553. }
  554.  
  555. function addClass(els, name) {
  556. let indx,
  557. len = els.length;
  558. for (indx = 0; indx < len; indx++) {
  559. els[indx].classList.add(name);
  560. }
  561. return len;
  562. }
  563.  
  564. function removeClass(els, name) {
  565. let indx,
  566. len = els.length;
  567. for (indx = 0; indx < len; indx++) {
  568. els[indx].classList.remove(name);
  569. }
  570. }
  571.  
  572. function toggleClass(els, name, flag) {
  573. els = Array.isArray(els) ? els : [els];
  574. let el,
  575. indx = els.length;
  576. while (indx--) {
  577. el = els[indx];
  578. if (el) {
  579. if (typeof flag === "undefined") {
  580. flag = !el.classList.contains(name);
  581. }
  582. if (flag) {
  583. el.classList.add(name);
  584. } else {
  585. el.classList.remove(name);
  586. }
  587. }
  588. }
  589. }
  590.  
  591. function init() {
  592. getSettings();
  593. addMenu();
  594. $("body").addEventListener("input", checkItem);
  595. $("body").addEventListener("click", checkItem);
  596. update();
  597. }
  598.  
  599. // update TOC when content changes
  600. document.addEventListener("ghmo:container", addMenu);
  601. document.addEventListener("ghmo:comments", update);
  602. init();
  603.  
  604. })();