Greasy Fork++

添加各种功能并改善 Greasy Fork 体验

目前为 2023-08-25 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Greasy Fork++
  3. // @author CY Fung <https://greasyfork.org/users/371179> & Davide <iFelix18@protonmail.com>
  4. // @namespace https://github.com/iFelix18
  5. // @icon https://www.google.com/s2/favicons?domain=https://greasyfork.org
  6. // @description Adds various features and improves the Greasy Fork experience
  7. // @description:de Fügt verschiedene Funktionen hinzu und verbessert das Greasy Fork-Erlebnis
  8. // @description:es Agrega varias funciones y mejora la experiencia de Greasy Fork
  9. // @description:fr Ajoute diverses fonctionnalités et améliore l'expérience Greasy Fork
  10. // @description:it Aggiunge varie funzionalità e migliora l'esperienza di Greasy Fork
  11. // @description:ru Добавляет различные функции и улучшает работу с Greasy Fork
  12. // @description:zh-CN 添加各种功能并改善 Greasy Fork 体验
  13. // @description:zh-TW 加入多種功能並改善Greasy Fork的體驗
  14. // @description:ja Greasy Forkの体験を向上させる様々な機能を追加
  15. // @description:ko Greasy Fork 경험을 향상시키고 다양한 기능을 추가
  16. // @copyright 2023, CY Fung (https://greasyfork.org/users/371179); 2021, Davide (https://github.com/iFelix18)
  17. // @license MIT
  18. // @version 3.0.10
  19. // @require https://fastly.jsdelivr.net/gh/sizzlemctwizzle/GM_config@06f2015c04db3aaab9717298394ca4f025802873/gm_config.js
  20. // @require https://fastly.jsdelivr.net/npm/@violentmonkey/shortcut@1.3.0/dist/index.min.js
  21. // @match *://greasyfork.org/*
  22. // @match *://sleazyfork.org/*
  23. // @connect greasyfork.org
  24. // @compatible chrome
  25. // @compatible edge
  26. // @compatible firefox
  27. // @compatible safari
  28. // @compatible brave
  29. // @grant GM.deleteValue
  30. // @grant GM.getValue
  31. // @grant GM.notification
  32. // @grant GM.registerMenuCommand
  33. // @grant GM.setValue
  34. // @run-at document-start
  35. // @inject-into page
  36. // ==/UserScript==
  37.  
  38. /* global GM_config, VM, GM */
  39.  
  40. // -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js --------
  41. // optimized by CY Fung to remove $ dependency and observe creation
  42. const UU = (function () {
  43. const scriptName = GM.info.script.name;
  44. const scriptVersion = GM.info.script.version;
  45. const authorMatch = /^(.*?)\s<\S[^\s@]*@\S[^\s.]*\.\S+>$/.exec(GM.info.script.author);
  46. const author = authorMatch ? authorMatch[1] : GM.info.script.author;
  47. let scriptId = scriptName.toLowerCase().replace(/\s/g, "-");
  48. let loggingEnabled = false;
  49.  
  50. const log = (message) => {
  51. if (loggingEnabled) {
  52. console.log(`${scriptName}:`, message);
  53. }
  54. };
  55.  
  56. const error = (message) => {
  57. console.error(`${scriptName}:`, message);
  58. };
  59.  
  60. const warn = (message) => {
  61. console.warn(`${scriptName}:`, message);
  62. };
  63.  
  64. const alert = (message) => {
  65. window.alert(`${scriptName}: ${message}`);
  66. };
  67.  
  68. /** @param {string} text */
  69. const short = (text, length) => {
  70. const s = text.split(" ");
  71. const l = Number(length);
  72. return s.length > l
  73. ? `${s.slice(0, l).join(" ")} [...]`
  74. : text;
  75. };
  76.  
  77. const addStyle = (css) => {
  78. const head = document.head || document.querySelector("head");
  79. const style = document.createElement("style");
  80. style.textContent = css;
  81. head.appendChild(style);
  82. };
  83.  
  84. const init = async (options = {}) => {
  85. scriptId = options.id || scriptId;
  86. loggingEnabled = typeof options.logging === "boolean" ? options.logging : false;
  87. console.info(
  88. `%c${scriptName}\n%cv${scriptVersion}${author ? ` by ${author}` : ""} is running!`,
  89. "color:red;font-weight:700;font-size:18px;text-transform:uppercase",
  90. ""
  91. );
  92. };
  93.  
  94. return {
  95. init,
  96. log,
  97. error,
  98. warn,
  99. alert,
  100. short,
  101. addStyle
  102. };
  103. })();
  104.  
  105. // -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js --------
  106.  
  107.  
  108. const mWindow = (() => {
  109.  
  110.  
  111. const fields = {
  112. hideBlacklistedScripts: {
  113. label: 'Hide blacklisted scripts:<br><span>Choose which lists to activate in the section below, press <b>Ctrl + Alt + B</b> to show Blacklisted scripts</span>',
  114. section: ['Features'],
  115. labelPos: 'right',
  116. type: 'checkbox',
  117. default: true
  118. },
  119. hideHiddenScript: {
  120. label: 'Hide scripts:<br><span>Add a button to hide the script<br>See and edit the list of hidden scripts below, press <b>Ctrl + Alt + H</b> to show Hidden script',
  121. labelPos: 'right',
  122. type: 'checkbox',
  123. default: true
  124. },
  125. showInstallButton: {
  126. label: 'Install button:<br><span>Add to the scripts list a button to install the script directly</span>',
  127. labelPos: 'right',
  128. type: 'checkbox',
  129. default: true
  130. },
  131. showTotalInstalls: {
  132. label: 'Installations:<br><span>Shows the number of daily and total installations on the user profile</span>',
  133. labelPos: 'right',
  134. type: 'checkbox',
  135. default: true
  136. },
  137. milestoneNotification: {
  138. label: 'Milestone notifications:<br><span>Get notified whenever your total installs got over any of these milestone<br>Separate milestones with a comma, leave blank to turn off notifications</span>',
  139. labelPos: 'left',
  140. type: 'text',
  141. title: 'Separate milestones with a comma!',
  142. size: 150,
  143. default: '10, 100, 500, 1000, 2500, 5000, 10000, 100000, 1000000'
  144. },
  145. nonLatins: {
  146. label: 'Non-Latin:<br><span>This list blocks all scripts with non-Latin characters in the title/description</span>',
  147. section: ['Lists'],
  148. labelPos: 'right',
  149. type: 'checkbox',
  150. default: false // not true
  151. },
  152. blacklist: {
  153. label: 'Blacklist:<br><span>A "non-opinionable" list that blocks all scripts with emoji in the title/description, references to "bots", "cheats" and some online game sites, and other "bullshit"</span>',
  154. labelPos: 'right',
  155. type: 'checkbox',
  156. default: true
  157. },
  158. customBlacklist: {
  159. label: 'Custom Blacklist:<br><span>Personal blacklist defined by a set of unwanted words<br>Separate unwanted words with a comma (example: YouTube, Facebook, pizza), leave blank to disable this list</span>',
  160. labelPos: 'left',
  161. type: 'text',
  162. title: 'Separate unwanted words with a comma!',
  163. size: 150,
  164. default: ''
  165. },
  166. hiddenList: {
  167. label: 'Hidden Scripts:<br><span>Block individual undesired scripts by their unique IDs<br>Separate IDs with a comma</span>',
  168. labelPos: 'left',
  169. type: 'textarea',
  170. title: 'Separate IDs with a comma!',
  171. default: '',
  172. save: false
  173. },
  174. logging: {
  175. label: 'Logging',
  176. section: ['Developer options'],
  177. labelPos: 'right',
  178. type: 'checkbox',
  179. default: false
  180. },
  181. debugging: {
  182. label: 'Debugging',
  183. labelPos: 'right',
  184. type: 'checkbox',
  185. default: false
  186. }
  187. }
  188.  
  189. const logo = 'data:image/png;base64,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'
  190.  
  191. const locales = { /* cSpell: disable */
  192. de: {
  193. downgrade: 'Auf zurückstufen',
  194. hide: '❌ Dieses skript ausblenden',
  195. install: 'Installieren',
  196. notHide: '✔️ Dieses skript nicht ausblenden',
  197. milestone: 'Herzlichen Glückwunsch, Ihre Skripte haben den Meilenstein von insgesamt $1 Installationen überschritten!',
  198. reinstall: 'Erneut installieren',
  199. update: 'Auf aktualisieren'
  200. },
  201. en: {
  202. downgrade: 'Downgrade to',
  203. hide: '❌ Hide this script',
  204. install: 'Install',
  205. notHide: '✔️ Not hide this script',
  206. milestone: 'Congrats, your scripts got over the milestone of $1 total installs!',
  207. reinstall: 'Reinstall',
  208. update: 'Update to'
  209. },
  210. es: {
  211. downgrade: 'Degradar a',
  212. hide: '❌ Ocultar este script',
  213. install: 'Instalar',
  214. notHide: '✔️ No ocultar este script',
  215. milestone: '¡Felicidades, sus scripts superaron el hito de $1 instalaciones totales!',
  216. reinstall: 'Reinstalar',
  217. update: 'Actualizar a'
  218. },
  219. fr: {
  220. downgrade: 'Revenir à',
  221. hide: '❌ Cacher ce script',
  222. install: 'Installer',
  223. notHide: '✔️ Ne pas cacher ce script',
  224. milestone: 'Félicitations, vos scripts ont franchi le cap des $1 installations au total!',
  225. reinstall: 'Réinstaller',
  226. update: 'Mettre à'
  227. },
  228. it: {
  229. downgrade: 'Riporta a',
  230. hide: '❌ Nascondi questo script',
  231. install: 'Installa',
  232. notHide: '✔️ Non nascondere questo script',
  233. milestone: 'Congratulazioni, i tuoi script hanno superato il traguardo di $1 installazioni totali!',
  234. reinstall: 'Reinstalla',
  235. update: 'Aggiorna a'
  236. },
  237. ru: {
  238. downgrade: 'Откатить до',
  239. hide: '❌ Скрыть этот скрипт',
  240. install: 'Установить',
  241. notHide: '✔️ Не скрывать этот сценарий',
  242. milestone: 'Поздравляем, ваши скрипты преодолели рубеж в $1 установок!',
  243. reinstall: 'Переустановить',
  244. update: 'Обновить до'
  245. },
  246. 'zh-CN': {
  247. downgrade: '降级到',
  248. hide: '❌ 隐藏此脚本',
  249. install: '安装',
  250. notHide: '✔️ 不隐藏此脚本',
  251. milestone: '恭喜,您的脚本超过了 $1 次总安装的里程碑!',
  252. reinstall: '重新安装',
  253. update: '更新到'
  254. },
  255. 'zh-TW': {
  256. downgrade: '降級至',
  257. hide: '❌ 隱藏此腳本',
  258. install: '安裝',
  259. notHide: '✔️ 不隱藏此腳本',
  260. milestone: '恭喜,您的腳本安裝總數已超過 $1!',
  261. reinstall: '重新安裝',
  262. update: '更新至'
  263. },
  264. 'ja': {
  265. downgrade: 'ダウングレードする',
  266. hide: '❌ このスクリプトを隠す',
  267. install: 'インストール',
  268. notHide: '✔️ このスクリプトを隠さない',
  269. milestone: 'おめでとうございます、あなたのスクリプトの合計インストール回数が $1 を超えました!',
  270. reinstall: '再インストール',
  271. update: '更新する'
  272. },
  273. 'ko': {
  274. downgrade: '다운그레이드하기',
  275. hide: '❌ 이 스크립트 숨기기',
  276. install: '설치',
  277. notHide: '✔️ 이 스크립트 숨기지 않기',
  278. milestone: '축하합니다, 스크립트의 총 설치 횟수가 $1을 넘었습니다!',
  279. reinstall: '재설치',
  280. update: '업데이트하기'
  281. }
  282.  
  283. };
  284.  
  285. const blacklist = [ /* cSpell: disable-next-line */
  286. '\\bagar((.)?io)?\\b', '\\bagma((.)?io)?\\b', '\\baimbot\\b', '\\barras((.)?io)?\\b', '\\bbot(s)?\\b', '\\bbubble((.)?am)?\\b', '\\bcheat(s)?\\b', '\\bdiep((.)?io)?\\b', '\\bfreebitco((.)?in)?\\b', '\\bgota((.)?io)?\\b', '\\bhack(s)?\\b', '\\bkrunker((.)?io)?\\b', '\\blostworld((.)?io)?\\b', '\\bmoomoo((.)?io)?\\b', '\\broblox(.com)?\\b', '\\bshell\\sshockers\\b', '\\bshellshock((.)?io)?\\b', '\\bshellshockers\\b', '\\bskribbl((.)?io)?\\b', '\\bslither((.)?io)?\\b', '\\bsurviv((.)?io)?\\b', '\\btaming((.)?io)?\\b', '\\bvenge((.)?io)?\\b', '\\bvertix((.)?io)?\\b', '\\bzombs((.)?io)?\\b', '\\p{Extended_Pictographic}'
  287. ];
  288.  
  289.  
  290. const settingsCSS = `
  291.  
  292. #greasyfork-plus{
  293. --config-var-display: flex;
  294. }
  295. #greasyfork-plus *{
  296. font-family:Open Sans,sans-serif,Segoe UI Emoji !important;
  297. font-size:12px
  298. }
  299. #greasyfork-plus .section_header{
  300. background-color:#670000;
  301. background-image:linear-gradient(#670000,#900);
  302. border:1px solid transparent;
  303. color:#fff
  304. }
  305. #greasyfork-plus .field_label[class]{
  306. margin-bottom:4px
  307. }
  308. #greasyfork-plus .field_label[class] span{
  309. font-size:95%;
  310. font-style:italic;
  311. opacity:.8;
  312. }
  313. #greasyfork-plus .field_label[class] b{
  314. color:#670000
  315. }
  316. #greasyfork-plus_logging_var[class],
  317. #greasyfork-plus_debugging_var[class] {
  318. --config-var-display: inline-flex;
  319. }
  320. #greasyfork-plus #greasyfork-plus_logging_var label.field_label[class],
  321. #greasyfork-plus #greasyfork-plus_debugging_var label.field_label[class] {
  322. margin-bottom:0;
  323. align-self: center;
  324. }
  325. #greasyfork-plus .config_var[class]{
  326. display:var(--config-var-display);
  327. }
  328. #greasyfork-plus_customBlacklist_var[class],
  329. #greasyfork-plus_hiddenList_var[class],
  330. #greasyfork-plus_milestoneNotification_var[class]{
  331. flex-direction:column;
  332. margin-left:21px;
  333. }
  334.  
  335. #greasyfork-plus_customBlacklist_var[class]::before,
  336. #greasyfork-plus_hiddenList_var[class]::before,
  337. #greasyfork-plus_milestoneNotification_var[class]::before{
  338. /* content: "◉"; */
  339. content: "◎";
  340. position: absolute;
  341. left: auto;
  342. top: auto;
  343. margin-left: -16px;
  344. }
  345. #greasyfork-plus_field_customBlacklist[class],
  346. #greasyfork-plus_field_milestoneNotification[class]{
  347. flex:1;
  348. }
  349. #greasyfork-plus_field_hiddenList[class]{
  350. box-sizing:border-box;
  351. overflow:hidden;
  352. resize:none;
  353. width:100%
  354. }
  355.  
  356. #greasyfork-plus_wrapper {
  357. box-sizing: border-box;
  358. overflow: auto;
  359. max-height: calc(100vh - 72px);
  360. padding: 12px;
  361. /* overflow: auto; */
  362. scrollbar-gutter: both-edges;
  363. background: rgba(127,127,127,0.05);
  364. border: 1px solid rgba(127,127,127,0.5);
  365. }
  366.  
  367. #greasyfork-plus_buttons_holder {
  368. position: fixed;
  369. bottom: 0;
  370. right: 0;
  371. margin: 0 12px 6px 0;
  372. }
  373.  
  374. #greasyfork-plus .saveclose_buttons[class] {
  375. padding: 4px 14px;
  376. margin: 6px;
  377. }
  378.  
  379. `;
  380.  
  381. const pageCSS = `
  382.  
  383. .script-list li.blacklisted{
  384. display:none;
  385. background:#321919;
  386. color:#e8e6e3
  387. }
  388. .script-list li.hidden{
  389. display:none;
  390. background:#321932;
  391. color:#e8e6e3
  392. }
  393. .script-list li.blacklisted a:not(.install-link),.script-list li.hidden a:not(.install-link){
  394. color:#ff8484
  395. }
  396. #script-info.hidden,#script-info.hidden .user-content{
  397. background:#321932;
  398. color:#e8e6e3
  399. }
  400. #script-info.hidden a:not(.install-link):not(.install-help-link){
  401. color:#ff8484
  402. }
  403. #script-info.hidden code{
  404. background-color:transparent
  405. }
  406. html {
  407. --block-btn-color:#111;
  408. --block-btn-bgcolor:#eee;
  409. }
  410. #script-info.hidden, #script-info.hidden .user-content {
  411. --block-btn-color:#eee;
  412. --block-btn-bgcolor:#111;
  413. }
  414.  
  415. [style-54998]{
  416. float:right;
  417. transform: scale(0.7);
  418. text-decoration:none
  419. }
  420.  
  421. [style-16377]{
  422. cursor:pointer;
  423. font-size:70%;
  424. white-space:nowrap;
  425. border: 1px solid #888;
  426. background: var(--block-btn-bgcolor, #eee);
  427. color: var(--block-btn-color);
  428. border-radius: 4px;
  429. padding: 0px 6px;
  430. }
  431. [style-77329] {
  432. cursor: pointer;
  433. margin-left: 1ex;
  434. white-space: nowrap;
  435. float: right;
  436. border: 1px solid #888;
  437. background: var(--block-btn-bgcolor, #eee);
  438. color: var(--block-btn-color);
  439. border-radius: 4px;
  440. padding: 0px 6px;
  441. }
  442.  
  443. a#hyperlink-35389,
  444. a#hyperlink-40361,
  445. a#hyperlink-35389:visited,
  446. a#hyperlink-40361:visited,
  447. a#hyperlink-35389:hover,
  448. a#hyperlink-40361:hover,
  449. a#hyperlink-35389:focus,
  450. a#hyperlink-40361:focus,
  451. a#hyperlink-35389:active,
  452. a#hyperlink-40361:active {
  453.  
  454. border: none !important;
  455. outline: none !important;
  456. box-shadow: none !important;
  457. appearance: none !important;
  458. background: none !important;
  459. color:inherit !important;
  460. }
  461.  
  462. a#hyperlink-35389{
  463. opacity: var(--hyperlink-blacklisted-option-opacity);
  464.  
  465. }
  466. a#hyperlink-40361{
  467. opacity: var(--hyperlink-hidden-option-opacity);
  468. }
  469.  
  470.  
  471. html {
  472.  
  473. --hyperlink-blacklisted-option-opacity: 0.5;
  474. --hyperlink-hidden-option-opacity: 0.5;
  475. }
  476.  
  477.  
  478. .list-option.list-current[class] > a[href] {
  479.  
  480. text-decoration:none;
  481. }
  482.  
  483. html {
  484. --blacklisted-display: none;
  485. --hidden-display: none;
  486. }
  487.  
  488. [blacklisted-shown] {
  489. --blacklisted-display: list-item;
  490. --hyperlink-blacklisted-option-opacity: 1;
  491. }
  492. [hidden-shown] {
  493. --hidden-display: list-item;
  494. --hyperlink-hidden-option-opacity: 1;
  495. }
  496.  
  497. .script-list li.blacklisted{
  498. display: var(--blacklisted-display);
  499.  
  500. }
  501.  
  502. .script-list li.hidden{
  503. display: var(--hidden-display);
  504.  
  505. }
  506.  
  507. `
  508.  
  509.  
  510.  
  511.  
  512. return { fields, logo, locales, blacklist, settingsCSS, pageCSS }
  513.  
  514.  
  515.  
  516. })();
  517.  
  518. (async () => {
  519.  
  520. function fixValue(key, def, test) {
  521. return GM.getValue(key, def).then((v) => test(v) || GM.deleteValue(key))
  522. }
  523.  
  524. await Promise.all([
  525. fixValue('hiddenList', [], v => v && typeof v === 'object' && typeof v.length === 'number' && (v.length === 0 || typeof v[0] === 'number')),
  526. fixValue('lastMilestone', 0, v => v && typeof v === 'number' && v >= 0)
  527. ])
  528.  
  529. const id = 'greasyfork-plus';
  530. const title = `${GM.info.script.name} v${GM.info.script.version} Settings`;
  531. const fields = mWindow.fields;
  532. const logo = mWindow.logo;
  533. const nonLatins = /[^\p{Script=Latin}\p{Script=Common}\p{Script=Inherited}]/gu;
  534. const blacklist = new RegExp(mWindow.blacklist.join('|'), 'giu');
  535. const hiddenList = await GM.getValue('hiddenList', []);
  536. const lang = document.documentElement.lang;
  537. const locales = mWindow.locales;
  538.  
  539. const gmc = new GM_config({
  540. id,
  541. title,
  542. fields,
  543. css: mWindow.settingsCSS,
  544. events: {
  545. init: () => {
  546. gmc.initializedResolve && gmc.initializedResolve();
  547. gmc.initializedResolve = null;
  548. if (!Array.isArray(hiddenList)) {
  549. GM.deleteValue('hiddenList');
  550. setTimeout(() => window.location.reload(false), 500);
  551. }
  552.  
  553. if (GM.info.scriptHandler !== 'Userscripts') {
  554. GM.registerMenuCommand('Configure', () => gmc.open());
  555. }
  556. },
  557. open: async (document) => {
  558. const textarea = document.querySelector(`#${id}_field_hiddenList`);
  559.  
  560. const hiddenList = await GM.getValue('hiddenList', []);
  561. const unsavedHiddenList = gmc.get('hiddenList') !== '' ? gmc.get('hiddenList').split(',').map(Number) : [];
  562.  
  563. if ((hiddenList.filter(item => !unsavedHiddenList.includes(item)).length > 0 || unsavedHiddenList.filter(item => !hiddenList.includes(item)).length > 0) && hiddenList.length !== 0) {
  564. gmc.fields.hiddenList.value = hiddenList.sort((a, b) => a - b).join(', ');
  565.  
  566. gmc.close();
  567. gmc.open();
  568. }
  569.  
  570. const resize = (target) => {
  571. target.style.height = '';
  572. target.style.height = `${target.scrollHeight}px`;
  573. };
  574.  
  575. if (textarea) {
  576. resize(textarea);
  577. textarea.addEventListener('input', (event) => resize(event.target));
  578.  
  579. }
  580. },
  581. save: async (forgotten) => {
  582. const unsavedHiddenList = forgotten.hiddenList !== '' ? forgotten.hiddenList.split(',').map(Number).filter((element) => element !== 0) : undefined;
  583.  
  584. if (gmc.isOpen) {
  585. await GM.setValue('hiddenList', Array.from(unsavedHiddenList));
  586.  
  587. UU.alert('settings saved');
  588. gmc.close();
  589. setTimeout(() => window.location.reload(false), 500);
  590. }
  591. }
  592. }
  593. });
  594. gmc.initialized = new Promise(r => (gmc.initializedResolve = r));
  595. await gmc.initialized.then();
  596.  
  597. UU.init({ id, logging: gmc.get('logging') });
  598. UU.log(nonLatins);
  599. UU.log(blacklist);
  600. UU.log(hiddenList);
  601.  
  602. const { register } = VM.shortcut;
  603. register('ctrl-alt-s', () => {
  604. gmc.open();
  605. });
  606. register('ctrl-alt-b', () => {
  607. toggleListDisplayingItem('blacklisted')
  608. // blacklistedToggled = !blacklistedToggled;
  609. // toggleElementVisibility('.script-list li.blacklisted');
  610. });
  611. register('ctrl-alt-h', () => {
  612. toggleListDisplayingItem('hidden')
  613. // hiddenToggled = !hiddenToggled;
  614. // toggleElementVisibility('.script-list li.hidden');
  615. });
  616.  
  617. const addSettingsToMenu = () => {
  618. const menu = document.createElement('li');
  619. menu.classList.add(id);
  620. const link = document.createElement('a');
  621. link.setAttribute('href', '#');
  622. link.textContent = GM.info.script.name;
  623. menu.appendChild(link);
  624. let nav = document.querySelector('#site-nav > nav')
  625. nav && nav.insertBefore(menu, document.querySelector('#site-nav > nav > li:first-child'));
  626.  
  627. menu.addEventListener('click', (e) => {
  628. e.preventDefault();
  629. e.stopPropagation();
  630. e.stopImmediatePropagation();
  631. gmc.open();
  632. });
  633. };
  634.  
  635.  
  636. const toggleListDisplayingItem = (t) => {
  637.  
  638. const m = document.documentElement;
  639.  
  640. const p = t + '-shown';
  641. let currentIsShown = m.hasAttribute(p)
  642. if (!currentIsShown) {
  643. m.setAttribute(p, '')
  644. } else {
  645. m.removeAttribute(p)
  646. }
  647.  
  648. }
  649.  
  650. const createListOptionGroup = () => {
  651.  
  652. const html = `<div class="list-option-group" id="${id}-options">${GM.info.script.name} Lists:<ul>
  653. <li class="list-option blacklisted"><a href="#" id="hyperlink-35389"></a></li>
  654. <li class="list-option hidden"><a href="#" id="hyperlink-40361"></a></li>
  655. </ul></div>`;
  656. const firstOptionGroup = document.querySelector('.list-option-groups > div');
  657. firstOptionGroup && firstOptionGroup.insertAdjacentHTML('beforebegin', html);
  658.  
  659. const blacklistedOption = document.querySelector(`#${id}-options li.blacklisted`);
  660. blacklistedOption && blacklistedOption.addEventListener('click', (evt) => {
  661. evt.preventDefault();
  662. toggleListDisplayingItem('blacklisted');
  663. }, false);
  664.  
  665. const hiddenOption = document.querySelector(`#${id}-options li.hidden`);
  666. hiddenOption && hiddenOption.addEventListener('click', (evt) => {
  667. evt.preventDefault();
  668. toggleListDisplayingItem('hidden');
  669. }, false);
  670.  
  671. }
  672.  
  673. const addOptions = () => {
  674.  
  675. const gn = () => {
  676.  
  677. let aBlackList = document.querySelector('#hyperlink-35389');
  678. let aHidden = document.querySelector('#hyperlink-40361');
  679. if (!aBlackList || !aHidden) return;
  680. aBlackList.textContent = `Blacklisted scripts (${document.querySelectorAll('.script-list li.blacklisted').length})`;
  681. aHidden.textContent = `Hidden scripts (${document.querySelectorAll('.script-list li.hidden').length})`
  682.  
  683. }
  684. const callback = (entries) => {
  685. if (entries && entries.length >= 1) requestAnimationFrame(gn);
  686. }
  687.  
  688. const setupScriptList = async () => {
  689. let scriptList;
  690. let i = 8;
  691. while (i-- > 0) {
  692. scriptList = document.querySelector('.script-list li')
  693. if (scriptList) scriptList = scriptList.closest('.script-list')
  694. if (scriptList) break;
  695. await new Promise(r => requestAnimationFrame(r))
  696. }
  697. if (!scriptList) return;
  698. createListOptionGroup();
  699. const mo = new MutationObserver(callback);
  700. mo.observe(scriptList, { childList: true, subtree: true });
  701. gn();
  702. }
  703. setupScriptList();
  704.  
  705. };
  706.  
  707.  
  708. /**
  709. * Get script data from Greasy Fork API
  710. *
  711. * @param {number} id Script ID
  712. * @returns {Promise} Script data
  713. */
  714. let networkMP1 = Promise.resolve();
  715. let networkMP2 = Promise.resolve();
  716. let previousIsCache = false;
  717. // let ss = [];
  718. // var sum = function(nums) {
  719. // var total = 0;
  720. // for (var i = 0, len = nums.length; i < len; i++) total += nums[i];
  721. // return total;
  722. // };
  723. const getScriptData = async (id, noCache) => {
  724. if (!(id >= 0)) return Promise.resolve()
  725. const url = `https://${window.location.hostname}/scripts/${id}.json`;
  726. return new Promise((resolve, reject) => {
  727.  
  728. networkMP1 = networkMP1.then(() => new Promise(unlock => {
  729.  
  730. const maxAgeInSeconds = 900;
  731. const rd = previousIsCache ? 1 : Math.floor(Math.random() * 80 + 80);
  732. let fetchStart = 0;
  733. new Promise(r => setTimeout(r, rd))
  734. .then(() => {
  735. fetchStart = Date.now();
  736. })
  737. .then(() => fetch(url, noCache ? {
  738. method: 'GET',
  739. cache: 'reload',
  740. credentials: 'omit',
  741. headers: new Headers({
  742. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  743. })
  744. } : {
  745. method: 'GET',
  746. cache: 'force-cache',
  747. credentials: 'omit',
  748. headers: new Headers({
  749. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  750. }),
  751. }))
  752. .then((response) => {
  753.  
  754. let fetchStop = Date.now();
  755. // const dd = fetchStop - fetchStart;
  756. // dd (cache) = {min: 1, max: 8, avg: 3.7}
  757. // dd (normal) = {min: 136, max: 316, avg: 162.62}
  758.  
  759. // ss.push(dd)
  760. // ss.maxValue = Math.max(...ss);
  761. // ss.minValue = Math.min(...ss);
  762. // ss.avgValue = sum(ss)/ss.length;
  763. // console.log(dd)
  764. // console.log(ss)
  765. previousIsCache = (fetchStop - fetchStart) < (3.7 + 162.62) / 2;
  766. UU.log(`${response.status}: ${response.url}`)
  767. // UU.log(response)
  768. if (response.ok === true) {
  769. unlock();
  770. return response.json()
  771. }
  772. if (response.status === 503) {
  773. return new Promise(r => setTimeout(r, 270 + rd)).then(() => {
  774. unlock();
  775. return getScriptData(id, true);
  776. });
  777. }
  778. console.warn(response);
  779. new Promise(r => setTimeout(r, 470)).then(unlock); // reload later
  780. })
  781. .then((data) => resolve(data))
  782. .catch((e) => {
  783. unlock();
  784. UU.log(id, url)
  785. console.warn(e)
  786. // reject(e)
  787. })
  788.  
  789. })).catch(() => { })
  790.  
  791. });
  792. }
  793.  
  794. /**
  795. * Get user data from Greasy Fork API
  796. *
  797. * @param {string} userID User ID
  798. * @returns {Promise} User data
  799. */
  800. const getUserData = (userID, noCache) => {
  801.  
  802. if (!(userID >= 0)) return Promise.resolve()
  803.  
  804. const url = `https://${window.location.hostname}/users/${userID}.json`;
  805. return new Promise((resolve, reject) => {
  806.  
  807.  
  808. networkMP2 = networkMP2.then(() => new Promise(unlock => {
  809.  
  810. const maxAgeInSeconds = 900;
  811. const rd = Math.floor(Math.random() * 80 + 80);
  812.  
  813. new Promise(r => setTimeout(r, rd))
  814. .then(() => fetch(url, noCache ? {
  815. method: 'GET',
  816. cache: 'reload',
  817. credentials: 'omit',
  818. headers: new Headers({
  819. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  820. })
  821. } : {
  822. method: 'GET',
  823. cache: 'force-cache',
  824. credentials: 'omit',
  825. headers: new Headers({
  826. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  827. }),
  828. }))
  829. .then((response) => {
  830. UU.log(`${response.status}: ${response.url}`)
  831. if (response.ok === true) {
  832. unlock();
  833. return response.json()
  834. }
  835. if (response.status === 503) {
  836. return new Promise(r => setTimeout(r, 270 + rd)).then(() => {
  837. unlock();
  838. return getUserData(userID, true); // reload later
  839. });
  840. }
  841. console.warn(response);
  842. new Promise(r => setTimeout(r, 470)).then(unlock);
  843. })
  844. .then((data) => resolve(data))
  845. .catch((e) => {
  846. setTimeout(() => {
  847. unlock()
  848. }, 270)
  849. UU.log(userID, url)
  850. console.warn(e)
  851. // reject(e)
  852. })
  853.  
  854.  
  855.  
  856. })).catch(() => { })
  857.  
  858. });
  859. }
  860. const getTotalInstalls = (data) => {
  861. if (!data || !data.scripts) return;
  862. return new Promise((resolve, reject) => {
  863. const totalInstalls = [];
  864.  
  865. data.scripts.forEach((element) => {
  866. totalInstalls.push(parseInt(element.total_installs, 10));
  867. });
  868.  
  869. resolve(totalInstalls.reduce((a, b) => a + b, 0));
  870. });
  871. };
  872.  
  873.  
  874. const isInstalled = (name, namespace) => {
  875. return new Promise((resolve, reject) => {
  876. if (window.external && window.external.Violentmonkey) {
  877. window.external.Violentmonkey.isInstalled(name, namespace).then((data) => resolve(data));
  878. return;
  879. }
  880.  
  881. if (window.external && window.external.Tampermonkey) {
  882. window.external.Tampermonkey.isInstalled(name, namespace, (data) => {
  883. (data.installed) ? resolve(data.version) : resolve();
  884. });
  885. return;
  886. }
  887.  
  888. resolve();
  889. });
  890. };
  891.  
  892. const compareVersions = (v1, v2) => {
  893. if (!v1 || !v2) return;
  894. if (v1 === null || v2 === null) return;
  895. if (v1 === v2) return 0;
  896.  
  897. const sv1 = v1.split('.').map((index) => +index);
  898. const sv2 = v2.split('.').map((index) => +index);
  899.  
  900. for (let index = 0; index < Math.max(sv1.length, sv2.length); index++) {
  901. if (sv1[index] > sv2[index]) return 1;
  902. if (sv1[index] < sv2[index]) return -1;
  903. }
  904.  
  905. return 0;
  906. };
  907.  
  908.  
  909. /**
  910. * Return label for the hide script button
  911. *
  912. * @param {boolean} hidden Is hidden
  913. * @returns {string} Label
  914. */
  915. const blockLabel = (hidden) => {
  916. return hidden ? (locales[lang] ? locales[lang].notHide : locales.en.notHide) : (locales[lang] ? locales[lang].hide : locales.en.hide)
  917. }
  918.  
  919. /**
  920. * Return label for the install button
  921. *
  922. * @param {number} update Update value
  923. * @returns {string} Label
  924. */
  925. const installLabel = (update) => {
  926. switch (update) {
  927. case undefined: {
  928. return locales[lang] ? locales[lang].install : locales.en.install
  929. }
  930. case 1: {
  931. return locales[lang] ? locales[lang].update : locales.en.update
  932. }
  933. case -1: {
  934. return locales[lang] ? locales[lang].downgrade : locales.en.downgrade
  935. }
  936. default: {
  937. return locales[lang] ? locales[lang].reinstall : locales.en.reinstall
  938. }
  939. }
  940. }
  941.  
  942. const hideBlacklistedScript = (element, list) => {
  943. if (!element) return;
  944. const scriptLink = element.querySelector('.script-link')
  945.  
  946. const name = scriptLink ? scriptLink.textContent : '';
  947. const descriptionElem = element.querySelector('.script-description')
  948. const description = descriptionElem ? descriptionElem.textContent : '';
  949.  
  950. if (!name) return;
  951.  
  952. switch (list) {
  953. case 'nonLatins':
  954. if ((nonLatins.test(name) || nonLatins.test(description)) && !element.classList.contains('blacklisted')) {
  955. element.classList.add('blacklisted', 'non-latins');
  956. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  957. let scriptLink = element.querySelector('.script-link');
  958. if (scriptLink) { scriptLink.textContent += ' (non-latin)'; }
  959. }
  960. }
  961. break;
  962. case 'blacklist':
  963. if ((blacklist.test(name) || blacklist.test(description)) && !element.classList.contains('blacklisted')) {
  964. element.classList.add('blacklisted', 'blacklist');
  965. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  966. let scriptLink = element.querySelector('.script-link');
  967. if (scriptLink) { scriptLink.textContent += ' (blacklist)'; }
  968. }
  969. }
  970. break;
  971. case 'customBlacklist': {
  972. const customBlacklist = new RegExp(gmc.get('customBlacklist').replace(/\s/g, '').split(',').join('|'), 'giu');
  973. if ((customBlacklist.test(name) || customBlacklist.test(description)) && !element.classList.contains('blacklisted')) {
  974. element.classList.add('blacklisted', 'custom-blacklist');
  975. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  976. let scriptLink = element.querySelector('.script-link');
  977. if (scriptLink) { scriptLink.textContent += ' (custom-blacklist)'; }
  978. }
  979. }
  980. break;
  981. }
  982. default:
  983. UU.log('No blacklists');
  984. break;
  985. }
  986. };
  987.  
  988. const hideHiddenScript = (element, id, list) => {
  989. id = +id;
  990. if (!(id >= 0)) return;
  991.  
  992. const isInHiddenList = () => hiddenList.indexOf(id) !== -1;
  993. const updateScriptLink = (shouldHide) => {
  994. if (gmc.get('hideHiddenScript') && gmc.get('debugging')) {
  995. let scriptLink = element.querySelector('.script-link');
  996. if (scriptLink) {
  997. if (shouldHide) {
  998. scriptLink.innerHTML += ' (hidden)';
  999. } else {
  1000. scriptLink.innerHTML = scriptLink.innerHTML.replace(' (hidden)', '');
  1001. }
  1002. }
  1003. }
  1004. };
  1005.  
  1006. // Check for initial state and set it
  1007. if (isInHiddenList()) {
  1008. element.classList.add('hidden');
  1009. updateScriptLink(true);
  1010. }
  1011.  
  1012. // Add button to hide the script
  1013. const insertButtonHTML = (selector, html) => {
  1014. const target = element.querySelector(selector);
  1015. if (!target) return;
  1016. let p = document.createElement('template');
  1017. p.innerHTML = html;
  1018. target.parentNode.insertBefore(p.content.firstChild, target.nextSibling);
  1019. };
  1020.  
  1021. const isHidden = element.classList.contains('hidden');
  1022. const blockButtonHTML = `<span class=block-button role=button style-16377>${blockLabel(isHidden)}</span>`;
  1023. const blockButtonHeaderHTML = `<span class=block-button role=button style-77329 style="">${blockLabel(isHidden)}</span>`;
  1024.  
  1025. insertButtonHTML('.badge-js, .badge-css', blockButtonHTML);
  1026. insertButtonHTML('header h2', blockButtonHeaderHTML);
  1027.  
  1028. // Add event listener
  1029. const button = element.querySelector('.block-button');
  1030. if (button) {
  1031. button.addEventListener('click', (event) => {
  1032. event.stopPropagation();
  1033. event.stopImmediatePropagation();
  1034.  
  1035. if (!isInHiddenList()) {
  1036. hiddenList.push(id);
  1037. GM.setValue('hiddenList', hiddenList);
  1038.  
  1039. element.classList.add('hidden');
  1040. updateScriptLink(true);
  1041.  
  1042. } else {
  1043. const index = hiddenList.indexOf(id);
  1044. hiddenList.splice(index, 1);
  1045. GM.setValue('hiddenList', hiddenList);
  1046.  
  1047. element.classList.remove('hidden');
  1048. updateScriptLink(false);
  1049. }
  1050.  
  1051. const blockBtn = element.querySelector('.block-button');
  1052. if (blockBtn) blockBtn.textContent = blockLabel(element.classList.contains('hidden'));
  1053. });
  1054. }
  1055. };
  1056.  
  1057. const insertButtonHTML = (element, selector, html) => {
  1058. const target = element.querySelector(selector);
  1059. if (!target) return;
  1060. let p = document.createElement('template');
  1061. p.innerHTML = html;
  1062. target.parentNode.insertBefore(p.content.firstChild, target.nextSibling);
  1063. };
  1064.  
  1065. const addInstallButton = (element, url, label, version) => {
  1066. insertButtonHTML(element, '.badge-js, .badge-css', `<a class="install-link" href="${url}" style-54998>${label} ${version}</a>`);
  1067. };
  1068.  
  1069. const showInstallButton = async (scriptID, element) => {
  1070.  
  1071. const script = await getScriptData(scriptID);
  1072. if (!script) return;
  1073.  
  1074. const installed = await isInstalled(script.name, script.namespace)
  1075.  
  1076. const update = compareVersions(script.version, installed);
  1077. const label = installLabel(update);
  1078. addInstallButton(element, script.code_url, label, script.version);
  1079.  
  1080. }
  1081.  
  1082.  
  1083. const foundScriptList = async (scriptList) => {
  1084.  
  1085. let rid = 0;
  1086. let g = () => {
  1087. if (!scriptList || scriptList.isConnected !== true) return;
  1088.  
  1089. const scriptElements = scriptList.querySelectorAll('li[data-script-id]:not([e8kk])');
  1090.  
  1091. for (const element of scriptElements) {
  1092. element.setAttribute('e8kk', '1');
  1093.  
  1094. const scriptID = +element.getAttribute('data-script-id');
  1095. if (!(scriptID > 0)) continue;
  1096.  
  1097. // blacklisted scripts
  1098. if (gmc.get('nonLatins')) hideBlacklistedScript(element, 'nonLatins');
  1099. if (gmc.get('blacklist')) hideBlacklistedScript(element, 'blacklist');
  1100. if (gmc.get('customBlacklist')) hideBlacklistedScript(element, 'customBlacklist');
  1101.  
  1102. // hidden scripts
  1103. if (gmc.get('hideHiddenScript')) hideHiddenScript(element, scriptID, true);
  1104.  
  1105. // install button
  1106. if (gmc.get('showInstallButton')) {
  1107. showInstallButton(scriptID, element)
  1108. }
  1109. }
  1110.  
  1111. }
  1112. let f = (entries) => {
  1113. const tid = ++rid
  1114. if (entries && entries.length) requestAnimationFrame(() => {
  1115. if (tid === rid) g();
  1116. });
  1117. }
  1118. let mo = new MutationObserver(f);
  1119. mo.observe(scriptList, { subtree: true, childList: true });
  1120.  
  1121. g();
  1122.  
  1123. }
  1124.  
  1125. const onReady = async () => {
  1126. addSettingsToMenu();
  1127.  
  1128.  
  1129. setTimeout(() => {
  1130. let installBtn = document.querySelector('a[data-script-id][data-script-version]')
  1131. let scriptID = installBtn && installBtn.textContent ? +installBtn.getAttribute('data-script-id') : 0;
  1132. if (scriptID > 0) {
  1133. getScriptData(scriptID, true);
  1134. } else {
  1135.  
  1136.  
  1137. const userLink = document.querySelector('#site-nav .user-profile-link a[href]');
  1138. let userID = userLink.getAttribute('href');
  1139.  
  1140. userID = /users\/(\d+)/.exec(userID);
  1141. if (userID) userID = userID[1];
  1142. if (userID) {
  1143. userID = +userID;
  1144. if (userID > 0) {
  1145. getUserData(userID, true);
  1146. }
  1147. }
  1148.  
  1149.  
  1150. }
  1151. }, 740);
  1152.  
  1153. const userLink = document.querySelector('.user-profile-link a[href]');
  1154. const userID = userLink ? userLink.getAttribute('href') : undefined;
  1155.  
  1156. // blacklisted scripts / hidden scripts / install button
  1157. if (window.location.pathname !== userID && !/discussions/.test(window.location.pathname) && (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript') || gmc.get('showInstallButton'))) {
  1158.  
  1159. const scriptList = document.querySelector('.script-list');
  1160. if (scriptList) {
  1161. foundScriptList(scriptList);
  1162. } else {
  1163. const timeout = Date.now() + 3000;
  1164. /** @type {MutationObserver | null} */
  1165. let mo = null;
  1166. const mutationCallbackForScriptList = () => {
  1167. if (!mo) return;
  1168. const scriptList = document.querySelector('.script-list');
  1169. if (scriptList) {
  1170. mo.disconnect();
  1171. mo.takeRecords();
  1172. mo = null;
  1173. foundScriptList(scriptList);
  1174. } else if (Date.now() > timeout) {
  1175. mo.disconnect();
  1176. mo.takeRecords();
  1177. mo = null;
  1178. }
  1179. }
  1180. mo = new MutationObserver(mutationCallbackForScriptList);
  1181. mo.observe(document, { subtree: true, childList: true });
  1182. }
  1183.  
  1184.  
  1185. // hidden scripts on details page
  1186. if (gmc.get('hideHiddenScript') && document.querySelector('#script-info') && document.querySelector('#script-info .install-link[data-script-id]')) {
  1187. const id = +document.querySelector('#script-info .install-link[data-script-id]').getAttribute('data-script-id');
  1188. hideHiddenScript(document.querySelector('#script-info'), id, false);
  1189. }
  1190.  
  1191. // add options and style for blacklisted/hidden scripts
  1192. if (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript')) {
  1193. addOptions();
  1194. UU.addStyle(mWindow.pageCSS);
  1195. }
  1196. }
  1197.  
  1198. // total installs
  1199. if (gmc.get('showTotalInstalls') && document.querySelector('#user-script-list')) {
  1200. const dailyInstalls = [];
  1201. const totalInstalls = [];
  1202.  
  1203. const dailyInstallElements = document.querySelectorAll('#user-script-list li dd.script-list-daily-installs');
  1204. for (const element of dailyInstallElements) {
  1205. dailyInstalls.push(parseInt(element.textContent.replace(/\D/g, ''), 10));
  1206. }
  1207.  
  1208. const totalInstallElements = document.querySelectorAll('#user-script-list li dd.script-list-total-installs');
  1209. for (const element of totalInstallElements) {
  1210. totalInstalls.push(parseInt(element.textContent.replace(/\D/g, ''), 10));
  1211. }
  1212.  
  1213. const dailyInstallsSum = dailyInstalls.reduce((a, b) => a + b, 0);
  1214. const totalInstallsSum = totalInstalls.reduce((a, b) => a + b, 0);
  1215.  
  1216. const convertLi = (li) => {
  1217.  
  1218. if (!li) return null;
  1219. const a = li.firstElementChild
  1220. if (a === null) return li;
  1221. if (a === li.lastElementChild && a.nodeName === 'A') return a;
  1222.  
  1223.  
  1224. return null;
  1225. }
  1226.  
  1227. const dailyOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(1)'));
  1228. dailyOption && dailyOption.insertAdjacentHTML('beforeend', `<span> (${dailyInstallsSum.toLocaleString()})</span>`);
  1229.  
  1230. const totalOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(2)'));
  1231. totalOption && totalOption.insertAdjacentHTML('beforeend', `<span> (${totalInstallsSum.toLocaleString()})</span>`);
  1232. }
  1233.  
  1234. // milestone notification
  1235. if (gmc.get('milestoneNotification')) {
  1236. const milestones = gmc.get('milestoneNotification').replace(/\s/g, '').split(',').map(Number);
  1237.  
  1238. if (!userID) return;
  1239.  
  1240. const userData = await getUserData(+userID.match(/\d+(?=\D)/g));
  1241. if (!userData) return;
  1242.  
  1243. const [totalInstalls, lastMilestone] = await Promise.all([
  1244. getTotalInstalls(userData),
  1245. GM.getValue('lastMilestone', 0)]);
  1246.  
  1247. const milestone = milestones.filter(milestone => totalInstalls >= milestone).pop();
  1248.  
  1249. UU.log(`total installs are "${totalInstalls}", milestone reached is "${milestone}", last milestone reached is "${lastMilestone}"`);
  1250.  
  1251. if (milestone <= lastMilestone) return;
  1252.  
  1253. if (milestone && milestone >= 0) {
  1254.  
  1255.  
  1256. GM.setValue('lastMilestone', milestone);
  1257.  
  1258. const lang = document.documentElement.lang;
  1259. const text = (locales[lang] ? locales[lang].milestone : locales.en.milestone).replace('$1', milestone.toLocaleString());
  1260.  
  1261. if (GM.info.scriptHandler !== 'Userscripts' && typeof GM.notification === 'function') {
  1262. GM.notification({
  1263. text,
  1264. title: GM.info.script.name,
  1265. image: logo,
  1266. onclick: () => {
  1267. window.location = `https://${window.location.hostname}${userID}#user-script-list-section`;
  1268. }
  1269. });
  1270. } else {
  1271. UU.alert(text);
  1272. }
  1273.  
  1274. }
  1275.  
  1276. }
  1277. }
  1278.  
  1279.  
  1280.  
  1281. Promise.resolve().then(() => {
  1282. if (document.readyState !== 'loading') {
  1283. onReady();
  1284. } else {
  1285. window.addEventListener("DOMContentLoaded", onReady, false);
  1286. }
  1287. });
  1288.  
  1289. })();