Greasy Fork++

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

目前为 2025-02-22 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Greasy Fork++
  3. // @namespace https://github.com/iFelix18
  4. // @version 3.3.1
  5. // @author CY Fung <https://greasyfork.org/users/371179> & Davide <iFelix18@protonmail.com>
  6. // @icon https://www.google.com/s2/favicons?domain=https://greasyfork.org
  7. // @description Adds various features and improves the Greasy Fork experience
  8. // @description:de Fügt verschiedene Funktionen hinzu und verbessert das Greasy Fork-Erlebnis
  9. // @description:es Agrega varias funciones y mejora la experiencia de Greasy Fork
  10. // @description:fr Ajoute diverses fonctionnalités et améliore l'expérience Greasy Fork
  11. // @description:it Aggiunge varie funzionalità e migliora l'esperienza di Greasy Fork
  12. // @description:ru Добавляет различные функции и улучшает работу с Greasy Fork
  13. // @description:zh-CN 添加各种功能并改善 Greasy Fork 体验
  14. // @description:zh-TW 加入多種功能並改善Greasy Fork的體驗
  15. // @description:ja Greasy Forkの体験を向上させる様々な機能を追加
  16. // @description:ko Greasy Fork 경험을 향상시키고 다양한 기능을 추가
  17. // @copyright 2023, CY Fung (https://greasyfork.org/users/371179); 2021, Davide (https://github.com/iFelix18)
  18. // @license MIT
  19. // @require https://fastly.jsdelivr.net/gh/sizzlemctwizzle/GM_config@06f2015c04db3aaab9717298394ca4f025802873/gm_config.min.js
  20. // @require https://fastly.jsdelivr.net/npm/@violentmonkey/shortcut@1.4.1/dist/index.min.js
  21. // @require https://fastly.jsdelivr.net/gh/cyfung1031/userscript-supports@3fa07109efca28a21094488431363862ccd52d7c/library/WinComm.min.js
  22. // @match *://greasyfork.org/*
  23. // @match *://sleazyfork.org/*
  24. // @match *://cn-greasyfork.org/*
  25. // @match *://api.greasyfork.org/*
  26. // @match *://api.sleazyfork.org/*
  27. // @match *://api.cn-greasyfork.org/*
  28. // @connect greasyfork.org
  29. // @connect sleazyfork.org
  30. // @connect cn-greasyfork.org
  31. // @compatible chrome
  32. // @compatible edge
  33. // @compatible firefox
  34. // @compatible safari
  35. // @compatible brave
  36. // @grant GM.deleteValue
  37. // @grant GM.getValue
  38. // @grant GM.notification
  39. // @grant GM.registerMenuCommand
  40. // @grant GM.setValue
  41. // @grant unsafeWindow
  42. // @run-at document-start
  43. // @inject-into content
  44. // ==/UserScript==
  45.  
  46. /* global GM_config, VM, GM, WinComm */
  47.  
  48. const isInIframe = window !== top;
  49.  
  50. /**
  51. * @typedef { typeof import("./library/WinComm.js") } WinComm
  52. */
  53.  
  54. // console.log(GM)
  55.  
  56. /** @type {WinComm} */
  57. const WinComm = this.WinComm;
  58.  
  59. // -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js --------
  60. // optimized by CY Fung to remove $ dependency and observe creation
  61. const UU = isInIframe || (function () {
  62. const scriptName = GM.info.script.name; // not name_i18n
  63. const scriptVersion = GM.info.script.version;
  64. const authorMatch = /^(.*?)\s<\S[^\s@]*@\S[^\s.]*\.\S+>$/.exec(GM.info.script.author);
  65. const author = authorMatch ? authorMatch[1] : GM.info.script.author;
  66. let scriptId = scriptName.toLowerCase().replace(/\s/g, "-");
  67. let loggingEnabled = false;
  68.  
  69. const log = (message) => {
  70. if (loggingEnabled) {
  71. console.log(`${scriptName}:`, message);
  72. }
  73. };
  74.  
  75. const error = (message) => {
  76. console.error(`${scriptName}:`, message);
  77. };
  78.  
  79. const warn = (message) => {
  80. console.warn(`${scriptName}:`, message);
  81. };
  82.  
  83. const alert = (message) => {
  84. window.alert(`${scriptName}: ${message}`);
  85. };
  86.  
  87. /** @param {string} text */
  88. const short = (text, length) => {
  89. const s = text.split(" ");
  90. const l = Number(length);
  91. return s.length > l
  92. ? `${s.slice(0, l).join(" ")} [...]`
  93. : text;
  94. };
  95.  
  96. const addStyle = (css) => {
  97. const head = document.head || document.querySelector("head");
  98. const style = document.createElement("style");
  99. style.textContent = css;
  100. head.appendChild(style);
  101. };
  102.  
  103. const init = async (options = {}) => {
  104. scriptId = options.id || scriptId;
  105. loggingEnabled = typeof options.logging === "boolean" ? options.logging : false;
  106. console.info(
  107. `%c${scriptName}\n%cv${scriptVersion}${author ? ` by ${author}` : ""} is running!`,
  108. "color:red;font-weight:700;font-size:18px;text-transform:uppercase",
  109. ""
  110. );
  111. };
  112.  
  113. return {
  114. init,
  115. log,
  116. error,
  117. warn,
  118. alert,
  119. short,
  120. addStyle
  121. };
  122. })();
  123.  
  124. // -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js --------
  125.  
  126.  
  127. const mWindow = isInIframe || (() => {
  128.  
  129.  
  130. const fields = {
  131. hideBlacklistedScripts: {
  132. 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>',
  133. section: ['Features'],
  134. labelPos: 'right',
  135. type: 'checkbox',
  136. default: true
  137. },
  138. hideHiddenScript: {
  139. 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',
  140. labelPos: 'right',
  141. type: 'checkbox',
  142. default: true
  143. },
  144. showInstallButton: {
  145. label: 'Install button:<br><span>Add to the scripts list a button to install the script directly</span>',
  146. labelPos: 'right',
  147. type: 'checkbox',
  148. default: true
  149. },
  150. showTotalInstalls: {
  151. label: 'Installations:<br><span>Shows the number of daily and total installations on the user profile</span>',
  152. labelPos: 'right',
  153. type: 'checkbox',
  154. default: true
  155. },
  156. milestoneNotification: {
  157. 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>',
  158. labelPos: 'left',
  159. type: 'text',
  160. title: 'Separate milestones with a comma!',
  161. size: 150,
  162. default: '10, 100, 500, 1000, 2500, 5000, 10000, 100000, 1000000'
  163. },
  164. nonLatins: {
  165. label: 'Non-Latin:<br><span>This list blocks all scripts with non-Latin characters in the title/description</span>',
  166. section: ['Lists'],
  167. labelPos: 'right',
  168. type: 'checkbox',
  169. default: false // not true
  170. },
  171. blacklist: {
  172. label: 'Blacklist:<br><span>A "non-opinionable" list that blocks all scripts with specific words in the title/description, references to "bots", "cheats" and some online game sites, and other "bullshit"</span>',
  173. labelPos: 'right',
  174. type: 'checkbox',
  175. default: true
  176. },
  177. customBlacklist: {
  178. 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>',
  179. labelPos: 'left',
  180. type: 'text',
  181. title: 'Separate unwanted words with a comma!',
  182. size: 150,
  183. default: ''
  184. },
  185. hiddenList: {
  186. label: 'Hidden Scripts:<br><span>Block individual undesired scripts by their unique IDs<br>Separate IDs with a comma</span>',
  187. labelPos: 'left',
  188. type: 'textarea',
  189. title: 'Separate IDs with a comma!',
  190. default: '',
  191. save: false
  192. },
  193. hideRecentUsersWithin: {
  194. label: 'Hide Recent Users:<br><span>Hide new regeistered users within the last N hours - to avoid seeing comments from spam accounts</span>',
  195. labelPos: 'left',
  196. type: 'text',
  197. title: 'Number only. 0 means disabled. maximum is 168. (Suggested value: 48)',
  198. default: '0',
  199. size: 150
  200. },
  201. logging: {
  202. label: 'Logging',
  203. section: ['Developer options'],
  204. labelPos: 'right',
  205. type: 'checkbox',
  206. default: false
  207. },
  208. debugging: {
  209. label: 'Debugging',
  210. labelPos: 'right',
  211. type: 'checkbox',
  212. default: false
  213. }
  214. }
  215.  
  216. const logo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAASFBMVEVHcEwBAQEDAwMAAAACAgIBAQEAAAAREREDAwMBAQH///8WFhYuLi7U1NSdnZ1bW1vExMTq6uqtra309PRERETf399ycnKGhoaVOQEOAAAACnRSTlMAg87/rjLgE1rzhWrqxgAABexJREFUaN61WouSpCAMVPEJKCqi//+nF4IKKig6e1SduzfupEkT8oIkiRlVVdRpnmdlQ0hTZnme1kVV4Zvk96Fla8nH0ZSI8rP0Ks2uwi1Ilv4EURW5K5xS0slhMb/BkD0hrMk/q1HVeSP6QVILMFIY8wagn6ojTV5Xn8RnbFZaoAPQc9bR3gXQ/yaWvYYA8VfKKeXACZVnAE1V9o4on/izWPsb/q9Ji3j5OcrjhiCXohsAQso6lh6QL9qOEd6GAAbKYAInAFAiiqYC5LMeLIaFKeppR3h/BiAkj6CpLuEPmbbHngUBhFZsdAGiaUL5xLBzRrAAZBlk5wpnVJEohHTbuZoAD0uhMUu+uY/bLZHaryBCH4vQCuugbnSoYf5sk+llKWaEETT/Qu2TecmSHaF1KPT6gmkM4hNLLkIR2l/guAZK1fQrS3kVXmChEX5mKb0xICH/gKXrQtf2pbhlyfqFoL/1LUOVEbFwcsuSs5GfAcjJ8dVkknbafpYUfUXSQYWqRP81THcs8fbVMmTVaQU6ENNOdyxNgGRYmFsp2/mQaFiKzGeC1IcVmAjrDjq4LAF9RgdF13CAo3cTDRcAP2OOCjX6UAwCPpbWyGZsCWTMAM0YTGF2Eg0XAD8bramue9jocGVpi5y7LbUUVRO0dRINF2D9bN/PBSqgAizt8gHByJAUddEyTqa7rYF57oZkkgiYj48lYeVTuuh4Hw1A8pWhxr68snQYioOxHSm6A2gq1wuZz68suUMKELst8oCLfAew+rzMecmOLO251wYwa4CDmd4B8GyPM1YDlyXeUp8Gx412A9Chy6vP9cXO0kW+5e6N104vH68sXeW/jwzptss8OihFf1UAY2dVkgDCdQz8dfiv1m3sZek62rcIsJlr/5uADv1bhNqzxrcIb3VIkzz06m9YykMAM39kidIoAG+5R7icHlm6BViUVDqSZknpfd8NZh2MO1Xz+JKlcYsfZeK3UqjBTDRexn680PVoSxMFBiCST6RJJmXzg2FTegaPzyRWRWu9cERAHW4o6jANmPU0Ewwqe36wa8j1wyQLADHyk1FphM760H1sBY/+PtS5ECQTvucHynoapYPiZJKFDoSNnFxZYl0QYG2gQExtcJFN8LNl1voHOA++5yQelh5yVPhRopma8M3OALMO8p0GhgDT+lgKDatBhhvN5gcuRWaZJeQ8CzVBLmBLd2tgdrLND9xFxh9CW8JABYRSNQVYugJYK8rB2bn5gWOmaM4dzmXQVjvuidMzS3YfpEm9uPnBtp5yNFRJLRUTb9OaiN1x+06uk0q4+cG+U+SqCeoKLmMwrYkp1pYWRbUvgoDjDZng7EScG3/wSxAyK7+/Xvrgl974JZ1gp69r1Bc7LvUlXhEIsSxh4lWU5Ecdwixh6lhlhPwvlkyZlpIvCFEspW4B8h9YWguQYOZynzZEsJTvRWBPxwDABnKuXWJY2ovAKu8H9h7gkSXblqqFIB8AHlhyekbGUk2PYUbXtvgAXGnYjfWwNA+QcDHN3+x2Q2rngENgiSeeAUZfjDMVHkSn1m2GGBVwCh0d8NlfhJ4owiyE+VjiPV0WKQ7tHCxD1h6DeQ7PAMKWvUcERtt2PDakkio9f/1pkdcsxMOSLq7ldD5LAJf3BeCaCfQmDl57s/Xak4sHEJiPjOcdN4f61+n8CDDQaX/iIk8KcrOTDqCC4Km3tdw9AeBM1+dq1IqRE0stI8LbWk6K7AmAjYPeX/jEdF/qJtgpX+pDzfH9eCVunFyt1UEQUt8dUHwE2BE6b2f8A8I1WMxqGLQfyqu7I8zmOwBh08TJrfy36+ANw1XcQdrHEXOeWeTf5edRJ7JV+t/o+UKTc+hRxx8oF+lLaxKCvTmw1vcRshcAbGFZ8eFUv4kF4NnHewn5pM91sauv7z9gumDPPNgoobBq54/XHraLGyAZXPLqaFrnzIMpKoeR/3BxY7t6woWY2hYqZZ0u2DOPeZzZr1dP7OUZbk4MVE+wecrmqcn+5vLMevsneP3ncfwDNtu0vRpuz80AAAAASUVORK5CYII='
  217.  
  218. const locales = { /* cSpell: disable */
  219. de: {
  220. downgrade: 'Auf zurückstufen',
  221. hide: '❌ Dieses skript ausblenden',
  222. install: 'Installieren',
  223. notHide: '✔️ Dieses skript nicht ausblenden',
  224. milestone: 'Herzlichen Glückwunsch, Ihre Skripte haben den Meilenstein von insgesamt $1 Installationen überschritten!',
  225. reinstall: 'Erneut installieren',
  226. update: 'Auf aktualisieren'
  227. },
  228. en: {
  229. downgrade: 'Downgrade to',
  230. hide: '❌ Hide this script',
  231. install: 'Install',
  232. notHide: '✔️ Not hide this script',
  233. milestone: 'Congrats, your scripts got over the milestone of $1 total installs!',
  234. reinstall: 'Reinstall',
  235. update: 'Update to'
  236. },
  237. es: {
  238. downgrade: 'Degradar a',
  239. hide: '❌ Ocultar este script',
  240. install: 'Instalar',
  241. notHide: '✔️ No ocultar este script',
  242. milestone: '¡Felicidades, sus scripts superaron el hito de $1 instalaciones totales!',
  243. reinstall: 'Reinstalar',
  244. update: 'Actualizar a'
  245. },
  246. fr: {
  247. downgrade: 'Revenir à',
  248. hide: '❌ Cacher ce script',
  249. install: 'Installer',
  250. notHide: '✔️ Ne pas cacher ce script',
  251. milestone: 'Félicitations, vos scripts ont franchi le cap des $1 installations au total!',
  252. reinstall: 'Réinstaller',
  253. update: 'Mettre à'
  254. },
  255. it: {
  256. downgrade: 'Riporta a',
  257. hide: '❌ Nascondi questo script',
  258. install: 'Installa',
  259. notHide: '✔️ Non nascondere questo script',
  260. milestone: 'Congratulazioni, i tuoi script hanno superato il traguardo di $1 installazioni totali!',
  261. reinstall: 'Reinstalla',
  262. update: 'Aggiorna a'
  263. },
  264. ru: {
  265. downgrade: 'Откатить до',
  266. hide: '❌ Скрыть этот скрипт',
  267. install: 'Установить',
  268. notHide: '✔️ Не скрывать этот сценарий',
  269. milestone: 'Поздравляем, ваши скрипты преодолели рубеж в $1 установок!',
  270. reinstall: 'Переустановить',
  271. update: 'Обновить до'
  272. },
  273. 'zh-CN': {
  274. downgrade: '降级到',
  275. hide: '❌ 隐藏此脚本',
  276. install: '安装',
  277. notHide: '✔️ 不隐藏此脚本',
  278. milestone: '恭喜,您的脚本超过了 $1 次总安装的里程碑!',
  279. reinstall: '重新安装',
  280. update: '更新到'
  281. },
  282. 'zh-TW': {
  283. downgrade: '降級至',
  284. hide: '❌ 隱藏此腳本',
  285. install: '安裝',
  286. notHide: '✔️ 不隱藏此腳本',
  287. milestone: '恭喜,您的腳本安裝總數已超過 $1!',
  288. reinstall: '重新安裝',
  289. update: '更新至'
  290. },
  291. 'ja': {
  292. downgrade: 'ダウングレードする',
  293. hide: '❌ このスクリプトを隠す',
  294. install: 'インストール',
  295. notHide: '✔️ このスクリプトを隠さない',
  296. milestone: 'おめでとうございます、あなたのスクリプトの合計インストール回数が $1 を超えました!',
  297. reinstall: '再インストール',
  298. update: '更新する'
  299. },
  300. 'ko': {
  301. downgrade: '다운그레이드하기',
  302. hide: '❌ 이 스크립트 숨기기',
  303. install: '설치',
  304. notHide: '✔️ 이 스크립트 숨기지 않기',
  305. milestone: '축하합니다, 스크립트의 총 설치 횟수가 $1을 넘었습니다!',
  306. reinstall: '재설치',
  307. update: '업데이트하기'
  308. }
  309.  
  310. };
  311.  
  312. const blacklist = [
  313. '\\bagar((\\.)?io)?\\b', '\\bagma((\\.)?io)?\\b', '\\baimbot\\b', '\\barras((\\.)?io)?\\b', '\\bbot(s)?\\b',
  314. '\\bbubble((\\.)?am)?\\b', '\\bcheat(s)?\\b', '\\bdiep((\\.)?io)?\\b', '\\bfreebitco((\\.)?in)?\\b', '\\bgota((\\.)?io)?\\b',
  315. '\\bhack(s)?\\b', '\\bkrunker((\\.)?io)?\\b', '\\blostworld((\\.)?io)?\\b', '\\bmoomoo((\\.)?io)?\\b', '\\broblox(\\.com)?\\b',
  316. '\\bshell\\sshockers\\b', '\\bshellshock((\\.)?io)?\\b', '\\bshellshockers\\b', '\\bskribbl((\\.)?io)?\\b', '\\bslither((\\.)?io)?\\b',
  317. '\\bsurviv((\\.)?io)?\\b', '\\btaming((\\.)?io)?\\b', '\\bvenge((\\.)?io)?\\b', '\\bvertix((\\.)?io)?\\b', '\\bzombs((\\.)?io)?\\b',
  318. // '\\p{Extended_Pictographic}'
  319. ];
  320.  
  321.  
  322. const settingsCSS = `
  323.  
  324. /*
  325. #greasyfork-plus label::before {
  326. content:'';
  327. display:block;
  328. position:absolute;
  329. left:0;
  330. right:0;
  331. top:0;
  332. bottom:0;
  333. z-index:1;
  334. }
  335. #greasyfork-plus label {
  336. position:relative;
  337. z-index:0;
  338. }
  339. */
  340.  
  341. html {
  342. color: #222;
  343. background: #f9f9f9;
  344. }
  345.  
  346. #greasyfork-plus{
  347. --config-var-display: flex;
  348. }
  349. #greasyfork-plus * {
  350. font-family:Open Sans,sans-serif,Segoe UI Emoji !important;
  351. font-size:12px
  352. }
  353. #greasyfork-plus .section_header[class] {
  354. background-color:#670000;
  355. background-image:linear-gradient(#670000,#900);
  356. border:1px solid transparent;
  357. color:#fff
  358. }
  359. #greasyfork-plus .field_label[class]{
  360. margin-bottom:4px
  361. }
  362. #greasyfork-plus .field_label[class] span{
  363. font-size:95%;
  364. font-style:italic;
  365. opacity:.8;
  366. }
  367. #greasyfork-plus .field_label[class] b{
  368. color:#670000
  369. }
  370. #greasyfork-plus_logging_var[class],
  371. #greasyfork-plus_debugging_var[class] {
  372. --config-var-display: inline-flex;
  373. }
  374. #greasyfork-plus #greasyfork-plus_logging_var label.field_label[class],
  375. #greasyfork-plus #greasyfork-plus_debugging_var label.field_label[class] {
  376. margin-bottom:0;
  377. align-self: center;
  378. }
  379. #greasyfork-plus .config_var[class]{
  380. display:var(--config-var-display);
  381. position: relative;
  382. }
  383. #greasyfork-plus_customBlacklist_var[class],
  384. #greasyfork-plus_hiddenList_var[class],
  385. #greasyfork-plus_milestoneNotification_var[class],
  386. #greasyfork-plus_hideRecentUsersWithin_var[class]{
  387. flex-direction:column;
  388. margin-left:21px;
  389. }
  390.  
  391. #greasyfork-plus_customBlacklist_var[class]::before,
  392. #greasyfork-plus_hiddenList_var[class]::before,
  393. #greasyfork-plus_milestoneNotification_var[class]::before,
  394. #greasyfork-plus_hideRecentUsersWithin_var[class]::before{
  395. /* content: "◉"; */
  396. content: "◎";
  397. position: absolute;
  398. left: auto;
  399. top: auto;
  400. margin-left: -16px;
  401. }
  402. #greasyfork-plus_field_customBlacklist[class],
  403. #greasyfork-plus_field_milestoneNotification[class]{
  404. flex:1;
  405. }
  406. #greasyfork-plus_field_hiddenList[class]{
  407. box-sizing:border-box;
  408. overflow:hidden;
  409. resize:none;
  410. width:100%
  411. }
  412.  
  413. body > #greasyfork-plus_wrapper:only-child {
  414. box-sizing: border-box;
  415. overflow: auto;
  416. max-height: calc(100vh - 72px);
  417. padding: 12px;
  418. /* overflow: auto; */
  419. scrollbar-gutter: both-edges;
  420. background: rgba(127,127,127,0.05);
  421. border: 1px solid rgba(127,127,127,0.5);
  422. }
  423.  
  424. #greasyfork-plus_wrapper > #greasyfork-plus_buttons_holder:last-child {
  425. position: fixed;
  426. bottom: 0;
  427. right: 0;
  428. margin: 0 12px 6px 0;
  429. }
  430.  
  431. #greasyfork-plus .saveclose_buttons[class] {
  432. padding: 4px 14px;
  433. margin: 6px;
  434. }
  435. #greasyfork-plus .section_header_holder#greasyfork-plus_section_2[class] {
  436. position: fixed;
  437. left: 0;
  438. bottom: 0;
  439. margin: 8px;
  440. }
  441. #greasyfork-plus .section_header#greasyfork-plus_section_header_2[class] {
  442. background: #000;
  443. color: #eee;
  444. }
  445.  
  446. #greasyfork-plus_header[class]{
  447. font-size: 16pt;
  448. font-weight: bold;
  449. }
  450.  
  451. `;
  452.  
  453. const pageCSS = `
  454.  
  455. .script-list li.blacklisted{
  456. display:none;
  457. background:#321919;
  458. color:#e8e6e3
  459. }
  460. .script-list li.hidden{
  461. display:none;
  462. background:#321932;
  463. color:#e8e6e3
  464. }
  465. .script-list li.blacklisted a:not(.install-link),.script-list li.hidden a:not(.install-link){
  466. color:#ff8484
  467. }
  468. #script-info.hidden,#script-info.hidden .user-content{
  469. background:#321932;
  470. color:#e8e6e3
  471. }
  472. #script-info.hidden a:not(.install-link):not(.install-help-link){
  473. color:#ff8484
  474. }
  475. #script-info.hidden code{
  476. background-color:transparent
  477. }
  478. html {
  479. --block-btn-color:#111;
  480. --block-btn-bgcolor:#eee;
  481. }
  482. #script-info.hidden, #script-info.hidden .user-content {
  483. --block-btn-color:#eee;
  484. --block-btn-bgcolor:#111;
  485. }
  486.  
  487. [style-54998]{
  488. float:right;
  489. font-size: 70%;
  490. text-decoration:none;
  491. }
  492.  
  493. [style-16377]{
  494. cursor:pointer;
  495. font-size:70%;
  496. white-space:nowrap;
  497. border: 1px solid #888;
  498. background: var(--block-btn-bgcolor, #eee);
  499. color: var(--block-btn-color);
  500. border-radius: 4px;
  501. padding: 0px 6px;
  502. margin: 0 8px;
  503. }
  504. [style-77329] {
  505. cursor: pointer;
  506. margin-left: 1ex;
  507. white-space: nowrap;
  508. float: right;
  509. border: 1px solid #888;
  510. background: var(--block-btn-bgcolor, #eee);
  511. color: var(--block-btn-color);
  512. border-radius: 4px;
  513. padding: 0px 6px;
  514. }
  515.  
  516. a#hyperlink-35389,
  517. a#hyperlink-40361,
  518. a#hyperlink-35389:visited,
  519. a#hyperlink-40361:visited,
  520. a#hyperlink-35389:hover,
  521. a#hyperlink-40361:hover,
  522. a#hyperlink-35389:focus,
  523. a#hyperlink-40361:focus,
  524. a#hyperlink-35389:active,
  525. a#hyperlink-40361:active {
  526.  
  527. border: none !important;
  528. outline: none !important;
  529. box-shadow: none !important;
  530. appearance: none !important;
  531. background: none !important;
  532. color:inherit !important;
  533. }
  534.  
  535. a#hyperlink-35389{
  536. opacity: var(--hyperlink-blacklisted-option-opacity);
  537.  
  538. }
  539. a#hyperlink-40361{
  540. opacity: var(--hyperlink-hidden-option-opacity);
  541. }
  542.  
  543.  
  544. html {
  545.  
  546. --hyperlink-blacklisted-option-opacity: 0.5;
  547. --hyperlink-hidden-option-opacity: 0.5;
  548. }
  549.  
  550.  
  551. .list-option.list-current[class] > a[href] {
  552.  
  553. text-decoration:none;
  554. }
  555.  
  556. html {
  557. --blacklisted-display: none;
  558. --hidden-display: none;
  559. }
  560.  
  561. [blacklisted-shown] {
  562. --blacklisted-display: list-item;
  563. --hyperlink-blacklisted-option-opacity: 1;
  564. }
  565. [hidden-shown] {
  566. --hidden-display: list-item;
  567. --hyperlink-hidden-option-opacity: 1;
  568. }
  569.  
  570. .script-list li.blacklisted{
  571. display: var(--blacklisted-display);
  572.  
  573. }
  574.  
  575. .script-list li.hidden{
  576. display: var(--hidden-display);
  577.  
  578. }
  579.  
  580. .install-link.install-status-checking,
  581. .install-link.install-status-checking:visited,
  582. .install-link.install-status-checking:active,
  583. .install-link.install-status-checking:hover,
  584. .install-help-link.install-status-checking {
  585. background-color: #405458;
  586. }
  587.  
  588. div.previewable{
  589. display: flex;
  590. flex-direction: column;
  591. }
  592. .script-version-ainfo-span {
  593. align-self:end;
  594. font-size: 90%;
  595. padding: 4px 8px;
  596. margin: 0;
  597. }
  598. [style*="display:"] + .script-version-ainfo-span{
  599. display: none;
  600. }
  601.  
  602.  
  603. /* Greasy Fork Enhance - Flat Layout */
  604.  
  605. [greasyfork-enhance-k37*="|flat-layout|"] ol.script-list > li > article > h2 {
  606. width: 0;
  607. flex-grow: 1;
  608. flex-basis: 60%;
  609. }
  610.  
  611. [greasyfork-enhance-k37*="|flat-layout|"] ol.script-list > li > article > div.script-meta-block {
  612. width: auto;
  613. flex-basis: 40%;
  614. flex-shrink: 0;
  615. flex-grow: 0;
  616. }
  617.  
  618. [greasyfork-enhance-k37*="|flat-layout|"] .script-list li:not(.ad-entry) {
  619. padding: 1em;
  620. margin: 0;
  621. }
  622.  
  623. [greasyfork-enhance-k37*="|flat-layout|"] .script-list li:not(.ad-entry) article {
  624. padding: 0;
  625. margin: 0;
  626. }
  627.  
  628. [greasyfork-enhance-k37*="|flat-layout|"] #script-info div.script-meta-block + #additional-info {
  629.  
  630. max-width: calc( 100% - 340px );
  631. min-height: 300px;
  632. box-sizing: border-box;
  633. }
  634.  
  635. [greasyfork-enhance-k37*="|basic|"] ul.outline {
  636. margin-bottom: -99vh;
  637.  
  638. }
  639.  
  640. .discussion-list .hidden {
  641. display: none;
  642. }
  643.  
  644. /* Greasy Fork Empty Ad Block */
  645. .ethical-ads-text[class]:empty {
  646. min-height: unset;
  647. }
  648.  
  649. .discussion-item-by-recent-user{
  650. opacity: 0.2;
  651. }
  652.  
  653.  
  654. `
  655.  
  656. const window = {};
  657.  
  658. /** @param {typeof WinComm.createInstance} createInstance */
  659. function contentScriptText(shObject, createInstance) {
  660.  
  661. // avoid setupEthicalAdsFallback looping
  662. if (typeof window.ethicalads === "undefined") {
  663. const p = Promise.resolve([]);
  664. window.ethicalads = { wait: p };
  665. }
  666.  
  667. /*
  668. *
  669.  
  670. return new Promise((resolve, reject) => {
  671. const external = unsafeWindow.external;
  672. console.log(334, external)
  673. const scriptHandler = GM.info.scriptHandler;
  674. if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey' ) {
  675. external.Violentmonkey.isInstalled(name, namespace).then((data) => resolve(data));
  676. return;
  677. }
  678.  
  679. if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') {
  680. external.Tampermonkey.isInstalled(name, namespace, (data) => {
  681. (data.installed) ? resolve(data.version) : resolve();
  682. });
  683. return;
  684. }
  685.  
  686. resolve();
  687. });
  688.  
  689. */
  690.  
  691. if (document.querySelector('#greasyfork-enhance-basic')) {
  692.  
  693.  
  694.  
  695. const setScriptOnDisabled = async (style) => {
  696.  
  697. try {
  698. const pd = Object.getOwnPropertyDescriptor(style.constructor.prototype, 'disabled');
  699. const { get, set } = pd;
  700. Object.defineProperty(style, 'disabled', {
  701. get() {
  702. return get.call(this);
  703. },
  704. set(nv) {
  705. let r = set.call(this, nv);
  706. Promise.resolve().then(chHead);
  707. return r;
  708. }
  709. })
  710. } catch (e) {
  711.  
  712. }
  713. };
  714.  
  715. document.addEventListener('style-s48', function (evt) {
  716. const target = (evt || 0).target || 0;
  717. if (!target) return;
  718. setScriptOnDisabled(target)
  719.  
  720. }, true);
  721.  
  722.  
  723. const isScriptEnabled = (style) => {
  724.  
  725. if (style instanceof HTMLStyleElement) {
  726. if (!style.hasAttribute('s48')) {
  727. style.setAttribute('s48', '');
  728. style.dispatchEvent(new CustomEvent('style-s48'));
  729. // setScriptOnDisabled(style);
  730. }
  731. return style.disabled !== true;
  732. }
  733. return false;
  734. }
  735. const chHead = () => {
  736. let p = [];
  737. if (isScriptEnabled(document.getElementById('greasyfork-enhance-basic')))
  738. p.push('basic');
  739. if (isScriptEnabled(document.getElementById('greasyfork-enhance-flat-layout')))
  740. p.push('flat-layout');
  741. if (isScriptEnabled(document.getElementById('greasyfork-enhance-animation')))
  742. p.push('animation');
  743. if (p.length >= 1)
  744. document.documentElement.setAttribute('greasyfork-enhance-k37', `|${p.join('|')}|`);
  745. else
  746. document.documentElement.removeAttribute('greasyfork-enhance-k37');
  747. }
  748. const moHead = new MutationObserver(chHead);
  749. moHead.observe(document.head, { subtree: false, childList: true });
  750. chHead();
  751.  
  752. /*
  753. const outline = document.querySelector('aside.panel > ul.outline');
  754. if(outline) {
  755. const div = document.createElement('div');
  756. //outline.replaceWith(div);
  757. //div.appendChild(outline)
  758. }
  759. */
  760.  
  761. // Promise.resolve().then(()=>{
  762. // let outline = document.querySelector('[greasyfork-enhance-k37*="|basic|"] header + aside.panel ul.outline');
  763. // if(outline){
  764. // let aside = outline.closest('aside.panel');
  765. // let header = aside.parentNode.querySelector('header');
  766. // let p = header.getBoundingClientRect().height;
  767.  
  768. // document.body.parentNode.insertBefore(aside, document.body);
  769. // // outline.style.top='0'
  770. // p+=(parseFloat(getComputedStyle(outline).marginTop.replace('px',''))||0)
  771. // outline.style.marginTop= p.toFixed(2)+'px';
  772. // }
  773.  
  774. // })
  775.  
  776. }
  777.  
  778.  
  779.  
  780. const { scriptHandler, scriptName, scriptVersion, scriptNamespace, communicationId } = shObject;
  781.  
  782. const wincomm = createInstance(communicationId);
  783.  
  784. const external = window.external;
  785.  
  786. if (external[scriptHandler]) 1;
  787. else if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey') scriptHandler = 'Violentmonkey';
  788. else if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') scriptHandler = 'Tampermonkey';
  789.  
  790. const manager = external[scriptHandler];
  791.  
  792. if (!manager) {
  793.  
  794. wincomm.send('userScriptManagerNotDetected', {
  795. code: 1
  796. });
  797. return;
  798.  
  799. }
  800.  
  801. const promiseWrap = (x) => {
  802. // bug in FireFox + Violentmonkey
  803. if (typeof (x || 0) === 'object' && typeof x.then === 'function') return x; else return Promise.resolve(x);
  804. };
  805.  
  806.  
  807. const pnIsInstalled2 = (type, scriptName, scriptNamespace) => new Promise((resolve, reject) => {
  808. const resultPr = promiseWrap(manager.isInstalled(scriptName, scriptNamespace));
  809. resultPr.then((result) => resolve({
  810. type,
  811. result: typeof result === 'string' ? { version: result } : result
  812. })).catch(reject);
  813. }).catch(console.warn);
  814.  
  815.  
  816. const pnIsInstalled3 = (type, scriptName, scriptNamespace) => new Promise((resolve, reject) => {
  817. try {
  818. manager.isInstalled(scriptName, scriptNamespace, (result) => {
  819. resolve({
  820. type,
  821. result: typeof result === 'string' ? { version: result } : result
  822. });
  823. });
  824. } catch (e) {
  825. reject(e);
  826. }
  827. }).catch(console.warn);
  828.  
  829.  
  830.  
  831. const enableScriptInstallChecker = (r) => {
  832.  
  833. const { type, result } = r;
  834. let version = result.version;
  835. // console.log(type, result, version)
  836. if (version !== scriptVersion) return;
  837.  
  838. const pnIsInstalled = type < 25 ? pnIsInstalled2 : pnIsInstalled3;
  839.  
  840. wincomm.hook('_$GreasyFork$Msg$OnScriptInstallCheck', {
  841.  
  842. 'installedVersion.req': (d, evt) => {
  843. pnIsInstalled(type, d.data.name, d.data.namespace).then((r) => {
  844. if (r && 'result' in r) {
  845. wincomm.response(evt, 'installedVersion.res', {
  846. version: r.result ? (r.result.version || '') : ''
  847. });
  848. }
  849. })
  850. }
  851.  
  852. });
  853.  
  854. wincomm.send('ready', { type });
  855.  
  856. // console.log('enableScriptInstallChecker', r)
  857.  
  858.  
  859. }
  860.  
  861. const kl = manager.isInstalled.length;
  862.  
  863. if (!(kl === 2 || kl === 3)) return;
  864. const puds = kl === 2 ? [
  865. pnIsInstalled2(21, scriptName, scriptNamespace), // scriptName is GM.info.script.name not GM.info.script.name_i18n
  866. pnIsInstalled2(20, scriptName, '')
  867. ] : [
  868. pnIsInstalled3(31, scriptName, scriptNamespace),
  869. pnIsInstalled3(30, scriptName, '')
  870. ];
  871.  
  872. Promise.all(puds).then((rs) => {
  873. const [r1, r0] = rs;
  874. if (r0 && r0.result && r0.result.version) enableScriptInstallChecker(r0); // '3.1.4'
  875. else if (r1 && r1.result && r1.result.version) enableScriptInstallChecker(r1);
  876. });
  877.  
  878.  
  879.  
  880. // console.log(327, shObject, scriptHandler);
  881.  
  882. }
  883.  
  884.  
  885.  
  886. return { fields, logo, locales, blacklist, settingsCSS, pageCSS, contentScriptText }
  887.  
  888.  
  889.  
  890. })();
  891.  
  892. const inIframeFn = isInIframe ? async () => {
  893. if (window.name) {
  894. const uo = new URL(location.href);
  895. const id38 = uo.searchParams.get('id38');
  896. if (id38 && `iframe-${id38}` === window.name) {
  897.  
  898. const p38 = uo.searchParams.get('p38');
  899. const h38 = uo.searchParams.get('h38');
  900.  
  901. if (`${p38}:` === uo.protocol && `${h38}` === uo.hostname) {
  902. window.addEventListener('message', (evt)=>{
  903. if(evt && evt.data){
  904. const {id38: id38_, msg, args, fetchId} = evt.data;
  905. if(id38_ === id38){
  906. if(msg === 'fetch' && fetchId){
  907. const [url, options] = args;
  908. if(options && options.headers){
  909. options.headers = new Headers(options.headers);
  910. }
  911. fetch(url, options).then(async (response) => {
  912. let json = null;
  913. if (response.ok === true) {
  914. try {
  915. json = await response.json();
  916. } catch (e) { }
  917. }
  918. const res = {
  919. status: response.status,
  920. url: response.url,
  921. ok: response.ok,
  922. json
  923. };
  924. evt.source.postMessage({
  925. id38,
  926. fetchId,
  927. msg: 'fetchResponse',
  928. args: [res]
  929. }, '*')
  930. })
  931. }
  932. }
  933. }
  934. });
  935. top.postMessage({
  936. id38: id38,
  937. msg: 'ready'
  938. }, '*');
  939. }
  940. }
  941. }
  942.  
  943. } : () => { };
  944.  
  945. inIframeFn() || (async () => {
  946.  
  947. let rafPromise = null;
  948.  
  949. const getRafPromise = () => rafPromise || (rafPromise = new Promise(resolve => {
  950. requestAnimationFrame(hRes => {
  951. rafPromise = null;
  952. resolve(hRes);
  953. });
  954. }));
  955.  
  956. const isVaildURL = (url) => {
  957. if (!url || typeof url !== 'string' || url.length < 23) return;
  958. let obj = null;
  959. try {
  960. obj = new URL(url);
  961. } catch (e) {
  962. return false;
  963. }
  964. if (obj && obj.host === obj.hostname && !obj.port && (obj.protocol || '').startsWith('http') && obj.pathname) {
  965. return true;
  966. }
  967. return false;
  968. };
  969.  
  970. const installLinkPointerDownHandler = function (e) {
  971. if (!e || !e.isTrusted) return;
  972. const button = e.target || this;
  973. if (button.hasAttribute('acnmd')) return;
  974. const href = button.href;
  975. if (!href || !isVaildURL(href)) return;
  976. if (/\.js[^-.\w\d\s:\/\\]*$/.test(href)) {
  977. 0 && fetch(href, {
  978. method: "GET",
  979. cache: 'reload',
  980. redirect: "follow"
  981. }).then(() => {
  982. console.debug('code url reloaded', href);
  983. }).catch((e) => {
  984. console.debug(e);
  985. });
  986. const m = /^(https\:\/\/(cn-greasyfork|greasyfork|sleazyfork)\.org\/[_-\w\/]*scripts\/(\d+)[-\w%]*)(\/|$)/.exec(location.href)
  987. if (m && m[1]) {
  988. const href = `${m[1]}/code`
  989. 0 && fetch(href, {
  990. method: "GET",
  991. cache: 'reload',
  992. redirect: "follow"
  993. }).then(() => {
  994. console.debug('code url reloaded', href);
  995. }).catch((e) => {
  996. console.debug(e);
  997. });
  998. }
  999.  
  1000. if (m && m[3] && href.includes('.user.js')) {
  1001. const href = `https://${location.hostname}/scripts/${m[3]}-fetching/code/${crypto.randomUUID()}.user.js?version_=${Date.now()}`
  1002. 0 && fetch(href, {
  1003. method: "GET",
  1004. cache: 'reload',
  1005. redirect: "follow"
  1006. }).then(() => {
  1007. console.debug('code url reloaded', href);
  1008. }).catch((e) => {
  1009. console.debug(e);
  1010. });
  1011. }
  1012.  
  1013.  
  1014. }
  1015.  
  1016. button.setAttribute('acnmd', '');
  1017. };
  1018.  
  1019. const setupInstallLink = (button) => {
  1020. if (!button || button.className !== 'install-link' || button.nodeName !== "A" || !button.href) return button;
  1021. button.addEventListener('pointerdown', installLinkPointerDownHandler);
  1022. return button;
  1023. };
  1024.  
  1025. function fixValue(key, def, test) {
  1026. return GM.getValue(key, def).then((v) => test(v) || GM.deleteValue(key))
  1027. }
  1028.  
  1029. const isNaNx = Number.isNaN;
  1030.  
  1031. function numberArr(arrVal) {
  1032. if (!arrVal || typeof arrVal.length !== 'number') return [];
  1033. return arrVal.filter(e => typeof e === 'number' && !isNaNx(e))
  1034. }
  1035.  
  1036. const isScriptFirstUse = await GM.getValue('firstUse', true);
  1037. await Promise.all([
  1038. fixValue('hiddenList', [], v => v && typeof v === 'object' && typeof v.length === 'number' && (v.length === 0 || typeof v[0] === 'number')),
  1039. fixValue('lastMilestone', 0, v => v && typeof v === 'number' && v >= 0)
  1040. ])
  1041.  
  1042. function createRE(t, ...opt) {
  1043. try {
  1044. return new RegExp(t, ...opt);
  1045. } catch (e) { }
  1046. return null;
  1047. }
  1048.  
  1049. const ruleFn = function (text) {
  1050. /** @type {String[]} */
  1051. const { rules, regExpArr } = this;
  1052. let text0 = text.replace(/\uE084/g, '\uE084x');
  1053. let j = 0;
  1054. for (const rule of rules) {
  1055. let r = false;
  1056. if (!rule.includes('\uE084')) {
  1057. r = (text.toLocaleLowerCase("en-US").includes(rule.toLocaleLowerCase("en-US")));
  1058. } else {
  1059. const s = rule.split(/\uE084(\d+)r/);
  1060. r = s.every((t, i) => {
  1061. if (t === undefined || t.length === 0) return true;
  1062. if (i % 2) {
  1063. return regExpArr[+t].test(text0);
  1064. } else {
  1065. return text0.includes(t.trim());
  1066. }
  1067. });
  1068. }
  1069. if (r) return j;
  1070. j++;
  1071. }
  1072. }
  1073.  
  1074. /** @param {String} txtRule */
  1075. const preprocessRule = (txtRule) => {
  1076. const regExpArr = [];
  1077. txtRule = txtRule.replace(/\uE084/g, '\uE084x');
  1078. let maxCount = 800; // avoid deadloop
  1079. while (maxCount--) {
  1080. const idx1 = txtRule.search(/\bre\//);
  1081. if (idx1 < 0) break;
  1082. const str = txtRule.substring(idx1 + 3);
  1083. let idx2 = -1;
  1084. const searcher = /(.?)\//g;
  1085. let m;
  1086. while (m = searcher.exec(str)) {
  1087. if (m[1] === '\\') continue;
  1088. idx2 = searcher.lastIndex + idx1 + 3;
  1089. break;
  1090. }
  1091. if (idx2 < 0) break;
  1092. const optionStr = txtRule.substring(idx2);
  1093. const optionM = /^[a-z]+/.exec(optionStr);
  1094. const option = optionM ? optionM[0] : '';
  1095. const regexContent = txtRule.substring(idx1 + 2 + 1, idx2 - 1);
  1096. txtRule = `${txtRule.substring(0, idx1)}${('\uE084' + regExpArr.length + 'r')}${txtRule.substring(idx2 + option.length)}`;
  1097. regExpArr.push(new RegExp(regexContent, option));
  1098. }
  1099. const rules = txtRule.split(',').map(e => e.trim());
  1100. return ruleFn.bind({ rules, regExpArr });
  1101. }
  1102.  
  1103. const useHashedScriptName = true;
  1104. const fixLibraryScriptCodeLink = true;
  1105. const addAdditionInfoLengthHint = true;
  1106.  
  1107. const id = 'greasyfork-plus';
  1108. const title = `${GM.info.script.name} v${GM.info.script.version} Settings`;
  1109. const fields = mWindow.fields;
  1110. const logo = mWindow.logo;
  1111. const nonLatins = /[^\p{Script=Latin}\p{Script=Common}\p{Script=Inherited}]/gu;
  1112. const blacklist = createRE((mWindow.blacklist || []).filter(e => !!e).join('|'), 'giu');
  1113. const hiddenList = numberArr(await GM.getValue('hiddenList', []));
  1114. const lang = document.documentElement.lang;
  1115. const locales = mWindow.locales;
  1116.  
  1117. const _isBlackList = (text) => {
  1118. if (!text || typeof text !== 'string') return false;
  1119. if (text.includes('hack') && (text.includes('EXPERIMENT_FLAGS') || text.includes('yt.'))) return false;
  1120. return blacklist.test(text);
  1121. }
  1122. const isBlackList = (name, description) => {
  1123. // To be reviewed
  1124. if (!blacklist) return false;
  1125. return _isBlackList(name) || _isBlackList(description);
  1126. }
  1127.  
  1128. function hiddenListStrToArr(str) {
  1129. if (!str || typeof str !== 'string') str = '';
  1130. return [...new Set(str ? numberArr(str.split(',').map(e => parseInt(e))) : [])];
  1131. }
  1132.  
  1133. const gmc = new GM_config({
  1134. id,
  1135. title,
  1136. fields,
  1137. css: mWindow.settingsCSS,
  1138. events: {
  1139. init: () => {
  1140. gmc.initializedResolve && gmc.initializedResolve();
  1141. gmc.initializedResolve = null;
  1142.  
  1143. },
  1144. /** @param {Document} document */
  1145. open: async (document) => {
  1146. const textarea = document.querySelector(`#${id}_field_hiddenList`);
  1147.  
  1148. const hiddenSet = new Set(numberArr(await GM.getValue('hiddenList', [])));
  1149. if (hiddenSet.size !== 0) {
  1150. const unsavedHiddenList = hiddenListStrToArr(gmc.get('hiddenList'));
  1151. const unsavedHiddenSet = new Set(unsavedHiddenList);
  1152.  
  1153. const hasDifferentItems = [...hiddenSet].some(item => !unsavedHiddenSet.has(item)) || [...unsavedHiddenSet].some(item => !hiddenSet.has(item));
  1154.  
  1155. if (hasDifferentItems) {
  1156.  
  1157. gmc.fields.hiddenList.value = [...hiddenSet].sort((a, b) => a - b).join(', ');
  1158.  
  1159. gmc.close();
  1160. gmc.open();
  1161.  
  1162. }
  1163.  
  1164.  
  1165. }
  1166.  
  1167. const resize = (target) => {
  1168. target.style.height = '';
  1169. target.style.height = `${target.scrollHeight}px`;
  1170. };
  1171.  
  1172. if (textarea) {
  1173. resize(textarea);
  1174. textarea.addEventListener('input', (event) => resize(event.target));
  1175.  
  1176. }
  1177.  
  1178. document.body.addEventListener('mousedown', (event) => {
  1179. if (event.detail > 1 && !event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && !event.defaultPrevented) {
  1180. event.preventDefault();
  1181. event.stopPropagation();
  1182. event.stopImmediatePropagation();
  1183. }
  1184. }, true);
  1185. },
  1186. save: async (forgotten) => {
  1187.  
  1188. if (gmc.isOpen) {
  1189. await GM.setValue('hiddenList', hiddenListStrToArr(forgotten.hiddenList));
  1190.  
  1191. UU.alert('settings saved');
  1192. gmc.close();
  1193. setTimeout(() => window.location.reload(false), 500);
  1194. }
  1195. }
  1196. }
  1197. });
  1198. gmc.initialized = new Promise(r => (gmc.initializedResolve = r));
  1199. await gmc.initialized.then();
  1200. const customBlacklistRF = preprocessRule(gmc.get('customBlacklist') || '');
  1201.  
  1202. const valHideRecentUsersWithin_ = Math.floor(+gmc.get('hideRecentUsersWithin'));
  1203. const valHideRecentUsersWithin = valHideRecentUsersWithin_ > 168 ? 168 : valHideRecentUsersWithin_ > 0 ? valHideRecentUsersWithin_ : 0;
  1204.  
  1205. /**
  1206. * Inserts an element into a sorted array using the given comparator function.
  1207. *
  1208. * @param {Array} arr - The sorted array.
  1209. * @param {*} element - The new element to insert.
  1210. * @param {Function} comparator - A function that takes two arguments (a, b)
  1211. * and returns a negative number if a < b,
  1212. * zero if a === b, or a positive number if a > b.
  1213. */
  1214. function insertSorted(arr, element, comparator) {
  1215. let left = 0;
  1216. let right = arr.length;
  1217.  
  1218. // Use binary search to find the correct index for insertion.
  1219. while (left < right) {
  1220. const mid = Math.floor((left + right) / 2);
  1221. if (comparator(element, arr[mid]) < 0) {
  1222. right = mid;
  1223. } else {
  1224. left = mid + 1;
  1225. }
  1226. }
  1227.  
  1228. // Insert the element at the found index.
  1229. arr.splice(left, 0, element);
  1230. return arr;
  1231. }
  1232.  
  1233. function findIndexSorted(arr, element, comparator) {
  1234. let left = 0;
  1235. let right = arr.length;
  1236.  
  1237. // Use binary search to find the correct index for insertion.
  1238. while (left < right) {
  1239. const mid = Math.floor((left + right) / 2);
  1240. if (comparator(element, arr[mid]) < 0) {
  1241. right = mid;
  1242. } else {
  1243. left = mid + 1;
  1244. }
  1245. }
  1246.  
  1247. return left; // arr_j > target [ arr_(j-1) <= target ]
  1248.  
  1249. }
  1250.  
  1251. let targetHiddenRecentDateTime = 0;
  1252. let userCreations = [];
  1253. let recentUserMP = Promise.resolve(0);
  1254. const fetchUserCreations = () => {
  1255. if (sessionStorage.__TMP_userCreations682__) {
  1256. try {
  1257. return JSON.parse(sessionStorage.__TMP_userCreations682__);
  1258. // console.log(388, userCreations);
  1259. } catch (e) {
  1260. console.warn(e);
  1261. }
  1262. }
  1263. return [];
  1264. }
  1265. userCreations = fetchUserCreations();
  1266. const cleanupUserCreations = () => {
  1267.  
  1268. // in case the record in sessionStorage is modified by other instances as well.
  1269. {
  1270. let storedUserCreations = fetchUserCreations();
  1271. let encodedArrS = storedUserCreations.map(e => e.join(','));
  1272. let encodedArrC = userCreations.map(e => e.join(','));
  1273. let encodedSetC = new Set(encodedArrC);
  1274. let encodedArrSFiltered = encodedArrS.filter(e => !encodedSetC.has(e));
  1275. let elementsMissing = encodedArrSFiltered.map(e => e.split(',').map(d => +d));
  1276. for (const element of elementsMissing) {
  1277. insertSorted(userCreations, element, (a, b) => a[1] - b[1]);
  1278. }
  1279. }
  1280.  
  1281. // since targetHiddenRecentDateTime is expected monotonic increasing, small values are useless in checking.
  1282. let deleteCount = 0;
  1283. for (let i = 0; i < userCreations.length - 1; i++) {
  1284. if (userCreations[i][1] < targetHiddenRecentDateTime && userCreations[i + 1][1] < targetHiddenRecentDateTime) {
  1285. deleteCount++;
  1286. } else {
  1287. break;
  1288. }
  1289. }
  1290. if (deleteCount > 0) {
  1291. deleteCount === 1 ? userCreations.shift() : userCreations.splice(0, deleteCount);
  1292. }
  1293.  
  1294. // trim the cache array to "8 + HALF" element size
  1295. while (userCreations.length > 32) {
  1296. // remove idx 8, 10, 12, ... 32, etc.
  1297. // 33 -> 20; 34 -> 21; 35 -> 21 , 36 -> 22, ...
  1298. // len2 = Math.floor(len1 / 2) + 4
  1299. // 58 -> 33 -> 20
  1300. userCreations = userCreations.filter((e, idx) => {
  1301. if (idx < 8) return true;
  1302. return (idx % 2) === 1;
  1303. });
  1304. }
  1305.  
  1306. sessionStorage.__TMP_userCreations682__ = JSON.stringify(userCreations);
  1307. // console.log(1238, userCreations);
  1308.  
  1309. };
  1310. const mightHideDiscussionByRecentlyNewUser = async (userId) => {
  1311.  
  1312. let result = null;
  1313.  
  1314. const tryBeforeNetworkRequest = () => {
  1315.  
  1316. let idxJ = findIndexSorted(userCreations, [null, targetHiddenRecentDateTime], (a, b) => a[1] - b[1]);
  1317.  
  1318. // findIndexSorted's result is arr_j[1] > targetHiddenRecentDateTime; reduce index for equality case
  1319. while (idxJ > 0 && userCreations[idxJ - 1][1] >= targetHiddenRecentDateTime) {
  1320. idxJ--;
  1321. }
  1322.  
  1323. let newFrom = 0, oldFrom = 0;
  1324.  
  1325. if (idxJ >= 0 && idxJ < userCreations.length && userCreations[idxJ][1] >= targetHiddenRecentDateTime) {
  1326. newFrom = userCreations[idxJ][0];
  1327. }
  1328.  
  1329. if (newFrom > 0 && userId >= newFrom) {
  1330. // console.log('newForm -> isRecent', userId, userCreations[idxJ][0], userCreations[idxJ][1], 'target', targetHiddenRecentDateTime)
  1331. return (result = true);
  1332. }
  1333.  
  1334. if (idxJ > 0 && idxJ - 1 < userCreations.length && userCreations[idxJ - 1][1] < targetHiddenRecentDateTime) {
  1335. oldFrom = userCreations[idxJ - 1][0];
  1336. }
  1337.  
  1338. if (oldFrom > 0 && userId <= oldFrom) {
  1339. // console.log('oldFrom -> notRecent', userId, userCreations[idxJ-1][0], userCreations[idxJ-1][1], 'target', targetHiddenRecentDateTime)
  1340. return (result = false);
  1341. }
  1342.  
  1343. return { newFrom, oldFrom };
  1344.  
  1345. }
  1346.  
  1347. tryBeforeNetworkRequest();
  1348. if (result !== null) return result;
  1349.  
  1350. recentUserMP = recentUserMP.then(async () => {
  1351.  
  1352. try {
  1353.  
  1354. const { newFrom, oldFrom } = tryBeforeNetworkRequest();
  1355. if (result !== null) return result;
  1356.  
  1357. // console.log(505, newFrom, oldFrom, userId);
  1358.  
  1359. const userData = await getUserData(userId, false);
  1360. if (userData.id !== userId) return (result = false);
  1361. const insertData = [userId, +(new Date(userData.created_at))];
  1362. insertSorted(userCreations, insertData, (a, b) => a[1] - b[1]);
  1363.  
  1364. // console.log('regDate', insertData);
  1365.  
  1366. result = insertData[1] >= targetHiddenRecentDateTime;
  1367. cleanupUserCreations();
  1368.  
  1369. } catch (e) {
  1370. console.warn(e);
  1371. }
  1372.  
  1373. });
  1374.  
  1375. await recentUserMP.then();
  1376. return result;
  1377.  
  1378. }
  1379.  
  1380. if (typeof GM.registerMenuCommand === 'function') {
  1381. GM.registerMenuCommand('Configure', () => gmc.open());
  1382. GM.registerMenuCommand('Reset Everything', () => {
  1383. Promise.all([
  1384. GM.deleteValue('hiddenList'),
  1385. GM.deleteValue('lastMilestone'),
  1386. GM.deleteValue('firstUse')
  1387. ]).then(() => {
  1388. setTimeout(() => window.location.reload(false), 50);
  1389. })
  1390. });
  1391. }
  1392.  
  1393. UU.init({ id, logging: gmc.get('logging') });
  1394. UU.log(nonLatins);
  1395. UU.log(blacklist);
  1396. UU.log(hiddenList);
  1397.  
  1398. const _VM = (typeof VM !== 'undefined' ? VM : null) || {
  1399. shortcut: {
  1400. register: () => { }
  1401. }
  1402. };
  1403.  
  1404.  
  1405. function fixLibraryCodeURL(code_url) {
  1406. if (/\/scripts\/(\d+)(\-[^\/]+)\/code\//.test(code_url)) {
  1407. code_url = code_url.replace(/\/scripts\/(\d+)(\-[^\/]+)\/code\//, '/scripts/$1/code/');
  1408. let qm = code_url.indexOf('?');
  1409. let s1 = code_url.substring(0, qm);
  1410. let s2 = code_url.substring(qm + 1);
  1411. if (qm > 0) {
  1412. code_url = `${decodeURI(s1)}?${s2}`;
  1413. }
  1414. }
  1415. return code_url;
  1416. }
  1417.  
  1418. function setClickToSelect(elm) {
  1419. elm.addEventListener('click', function () {
  1420. if (`${window.getSelection()}` === "") {
  1421. if (typeof this.select === 'function') {
  1422. this.select();
  1423. } else {
  1424. const range = document.createRange(); // Create a range object
  1425. range.selectNode(this); // Select the text within the element
  1426. const selection = window.getSelection(); // Get the selection object
  1427. selection.removeAllRanges(); // First clear any existing selections
  1428. selection.addRange(range); // Add the new range to the selection
  1429. }
  1430. }
  1431. });
  1432. elm.addEventListener('drag', function (evt) {
  1433. evt.preventDefault();
  1434. });
  1435. elm.addEventListener('drop', function (evt) {
  1436. evt.preventDefault();
  1437. });
  1438. elm.addEventListener('dragstart', function (evt) {
  1439. evt.preventDefault();
  1440. });
  1441. }
  1442.  
  1443. const copyText = typeof (((window.navigator || 0).clipboard || 0).writeText) === 'function' ? (text) => {
  1444. navigator.clipboard.writeText(text).then(function () {
  1445. //
  1446. }).catch(function (err) {
  1447. alert("Unable to Copy");
  1448. });
  1449. } : (text) => {
  1450. const textToCopy = document.createElement('strong');
  1451. textToCopy.style.position = 'fixed';
  1452. textToCopy.style.opacity = '0';
  1453. textToCopy.style.top = '-900vh';
  1454. textToCopy.textContent = text;
  1455. document.body.appendChild(textToCopy);
  1456.  
  1457. const range = document.createRange(); // Create a range object
  1458. range.selectNode(textToCopy); // Select the text within the element
  1459.  
  1460. const selection = window.getSelection(); // Get the selection object
  1461. selection.removeAllRanges(); // First clear any existing selections
  1462. selection.addRange(range); // Add the new range to the selection
  1463.  
  1464. try {
  1465. document.execCommand('copy'); // Try to copy the selected text
  1466. } catch (err) {
  1467. alert("Unable to Copy");
  1468. }
  1469.  
  1470. selection.removeAllRanges(); // Remove the selection range after copying
  1471. textToCopy.remove();
  1472. };
  1473.  
  1474.  
  1475. let avoidDuplication = 0;
  1476. const avoidDuplicationF = () => {
  1477. const p = avoidDuplication;
  1478. avoidDuplication = Date.now();
  1479. if (avoidDuplication - p < 30) return false;
  1480. return true;
  1481. }
  1482. // https://violentmonkey.github.io/vm-shortcut/
  1483. const shortcuts = [
  1484. ['ctrlcmd-alt-keys', () => avoidDuplicationF() && gmc.open()],
  1485. ['ctrlcmd-alt-keyb', () => avoidDuplicationF() && toggleListDisplayingItem('blacklisted')],
  1486. ['ctrlcmd-alt-keyh', () => avoidDuplicationF() && toggleListDisplayingItem('hidden')]
  1487. ]
  1488. for (const [scKey, scFn] of shortcuts) {
  1489. _VM.shortcut.register(scKey, scFn);
  1490. }
  1491.  
  1492. const addSettingsToMenu = () => {
  1493. const nav = document.querySelector('#site-nav > nav')
  1494. if (!nav) return;
  1495.  
  1496. const scriptName = GM.info.script.name;
  1497. const scriptVersion = GM.info.script.version;
  1498. const menu = document.createElement('li');
  1499. menu.classList.add(id);
  1500. menu.setAttribute('alt', `${scriptName} ${scriptVersion}`);
  1501. menu.setAttribute('title', `${scriptName} ${scriptVersion}`);
  1502. const link = document.createElement('a');
  1503. link.setAttribute('href', '#');
  1504. link.textContent = GM.info.script.name;
  1505. menu.appendChild(link);
  1506. nav.insertBefore(menu, document.querySelector('#site-nav > nav > li:first-of-type'));
  1507.  
  1508. menu.addEventListener('click', (e) => {
  1509. e.preventDefault();
  1510. e.stopPropagation();
  1511. e.stopImmediatePropagation();
  1512. gmc.open();
  1513. });
  1514. };
  1515.  
  1516.  
  1517. const toggleListDisplayingItem = (t) => {
  1518.  
  1519. const m = document.documentElement;
  1520.  
  1521. const p = t + '-shown';
  1522. let currentIsShown = m.hasAttribute(p)
  1523. if (!currentIsShown) {
  1524. m.setAttribute(p, '')
  1525. } else {
  1526. m.removeAttribute(p)
  1527. }
  1528.  
  1529. }
  1530.  
  1531. const createListOptionGroup = () => {
  1532.  
  1533. const html = `<div class="list-option-group" id="${id}-options">${GM.info.script.name} Lists:<ul>
  1534. <li class="list-option blacklisted"><a href="#" id="hyperlink-35389"></a></li>
  1535. <li class="list-option hidden"><a href="#" id="hyperlink-40361"></a></li>
  1536. </ul></div>`;
  1537. const firstOptionGroup = document.querySelector('.list-option-groups > div');
  1538. firstOptionGroup && firstOptionGroup.insertAdjacentHTML('beforebegin', html);
  1539.  
  1540. const blacklistedOption = document.querySelector(`#${id}-options li.blacklisted`);
  1541. blacklistedOption && blacklistedOption.addEventListener('click', (evt) => {
  1542. evt.preventDefault();
  1543. toggleListDisplayingItem('blacklisted');
  1544. }, false);
  1545.  
  1546. const hiddenOption = document.querySelector(`#${id}-options li.hidden`);
  1547. hiddenOption && hiddenOption.addEventListener('click', (evt) => {
  1548. evt.preventDefault();
  1549. toggleListDisplayingItem('hidden');
  1550. }, false);
  1551.  
  1552. }
  1553.  
  1554. const addOptions = () => {
  1555.  
  1556. const gn = () => {
  1557.  
  1558. let aBlackList = document.querySelector('#hyperlink-35389');
  1559. let aHidden = document.querySelector('#hyperlink-40361');
  1560. if (!aBlackList || !aHidden) return;
  1561. aBlackList.textContent = `Blacklisted scripts (${document.querySelectorAll('.script-list li.blacklisted').length})`;
  1562. aHidden.textContent = `Hidden scripts (${document.querySelectorAll('.script-list li.hidden').length})`
  1563.  
  1564. }
  1565. const callback = (entries) => {
  1566. if (entries && entries.length >= 1) requestAnimationFrame(gn);
  1567. }
  1568.  
  1569. const setupScriptList = async () => {
  1570. let scriptList;
  1571. let i = 8;
  1572. while (i-- > 0) {
  1573. scriptList = document.querySelector('.script-list li')
  1574. if (scriptList) scriptList = scriptList.closest('.script-list')
  1575. if (scriptList) break;
  1576. await new Promise(r => requestAnimationFrame(r))
  1577. }
  1578. if (!scriptList) return;
  1579. createListOptionGroup();
  1580. const mo = new MutationObserver(callback);
  1581. mo.observe(scriptList, { childList: true, subtree: true });
  1582. gn();
  1583. }
  1584. setupScriptList();
  1585.  
  1586. };
  1587.  
  1588.  
  1589. const PromiseExternal = ((resolve_, reject_) => {
  1590. const h = (resolve, reject) => { resolve_ = resolve; reject_ = reject };
  1591. return class PromiseExternal extends Promise {
  1592. constructor(cb = h) {
  1593. super(cb);
  1594. if (cb === h) {
  1595. /** @type {(value: any) => void} */
  1596. this.resolve = resolve_;
  1597. /** @type {(reason?: any) => void} */
  1598. this.reject = reject_;
  1599. }
  1600. }
  1601. };
  1602. })();
  1603.  
  1604. const corsFetchMap = new Map();
  1605.  
  1606. const corsFetch = async (url, options) => {
  1607. if (top !== window) return;
  1608. const uo = new URL(url);
  1609. const protocol = uo.protocol.replace(/[^\w]+/g, '');
  1610. const hostname = uo.hostname;
  1611. const origin0 = `${protocol}://${hostname}`;
  1612. let promiseF = null;
  1613. let prFn = corsFetchMap.get(origin0);
  1614. for (let i = 0; i < 2; i++) {
  1615. if (!prFn) {
  1616. prFn = new Promise((resolve) => {
  1617. let iframe = document.createElement('iframe');
  1618. const rid = `${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`;
  1619. iframe.id = `iframe-${rid}`;
  1620. iframe.name = `iframe-${rid}`;
  1621. window.addEventListener('message', (evt) => {
  1622. if (evt && evt.origin === origin0) {
  1623. const data = evt.data;
  1624. if (data && data.id38) {
  1625. const { id38, msg, fetchId: fetchId_, args } = data;
  1626. if (msg === 'ready') {
  1627. const iframeWindow = evt.source;
  1628. resolve((...args) => {
  1629. if (!iframe.isConnected) return -1;
  1630. const fetchId = `${Math.floor(Math.random() * 314159265359 + 314159265359).toString(36)}`;
  1631. const promise = new PromiseExternal();
  1632. corsFetchMap.set(`${id38}-${fetchId}`, promise);
  1633. iframeWindow.postMessage({
  1634. id38,
  1635. msg: 'fetch',
  1636. fetchId,
  1637. args
  1638. }, '*');
  1639. return promise;
  1640. });
  1641. } else if (msg === 'fetchResponse') {
  1642. const promise = corsFetchMap.get(`${id38}-${fetchId_}`);
  1643. if (promise) {
  1644. corsFetchMap.delete(`${id38}-${fetchId_}`);
  1645. promise.resolve(args[0]);
  1646. }
  1647. }
  1648. }
  1649. }
  1650. });
  1651. iframe.src = `${protocol}://${hostname}/robots.txt?id38=${rid}&p38=${protocol}&h38=${hostname}`;
  1652. Object.assign(iframe.style, {
  1653. 'position': 'fixed',
  1654. 'left': '-300px',
  1655. 'top': '-300px',
  1656. 'width': '30px',
  1657. 'height': '30px',
  1658. 'pointerEvents': 'none',
  1659. 'zIndex': '-1',
  1660. 'contain': 'strict'
  1661. });
  1662. (document.body || document.documentElement).appendChild(iframe);
  1663. });
  1664. corsFetchMap.set(origin0, prFn);
  1665. }
  1666. const fetchFn = await prFn.then();
  1667. const promise = fetchFn(url, options);
  1668. if (promise === -1) {
  1669. corsFetchMap.delete(origin0);
  1670. prFn = null;
  1671. continue;
  1672. }
  1673. if (promise && typeof promise.then === 'function') {
  1674. promiseF = promise;
  1675. break;
  1676. }
  1677. }
  1678. if (!promiseF) return null;
  1679. const promiseResult = await promiseF.then();
  1680. return promiseResult;
  1681. };
  1682.  
  1683. const standardFetch = async (url, options) => {
  1684. if (options && options.headers) {
  1685. options.headers = new Headers(options.headers);
  1686. }
  1687. const response = await fetch(url, options);
  1688. let json = null;
  1689. if (response.ok === true) {
  1690. try {
  1691. json = await response.json();
  1692. } catch (e) { }
  1693. }
  1694. const res = {
  1695. status: response.status,
  1696. url: response.url,
  1697. ok: response.ok,
  1698. json
  1699. };
  1700. return res;
  1701. }
  1702.  
  1703. /**
  1704. * Get script data from Greasy Fork API
  1705. *
  1706. * @param {number} id Script ID
  1707. * @returns {Promise} Script data
  1708. */
  1709. let networkMP1 = Promise.resolve();
  1710. let networkMP2 = Promise.resolve();
  1711. let previousIsCache = false;
  1712. // let ss = [];
  1713. // var sum = function(nums) {
  1714. // var total = 0;
  1715. // for (var i = 0, len = nums.length; i < len; i++) total += nums[i];
  1716. // return total;
  1717. // };
  1718. let reqStoresA = new Map();
  1719. let reqStoresB = new Map();
  1720.  
  1721. const getOldestEntry = (noCache)=>{
  1722. const reqStores = noCache ? reqStoresB : reqStoresA;
  1723. const oldestEntry = reqStores.entries().next();
  1724. if(!oldestEntry || !oldestEntry.value) return [];
  1725. const id = oldestEntry.value[0]
  1726. const req = oldestEntry.value[1]
  1727. reqStores.delete(id);
  1728. return [id, req];
  1729. }
  1730.  
  1731. let mutexC = Promise.resolve();
  1732. const getScriptDataAN = (noCache)=>{
  1733.  
  1734. mutexC = mutexC.then(async () => {
  1735.  
  1736. const [id, req] = getOldestEntry(noCache);
  1737.  
  1738. if (!(id > 0)) return;
  1739.  
  1740. const DO_CORS = /^(cn-greasyfork|greasyfork|sleazyfork)\.org$/.test(window.location.hostname) ? `api.${window.location.hostname}` : '';
  1741. const url = `https://${DO_CORS || window.location.hostname}/scripts/${id}.json`;
  1742. const fetchUrl = sessionStorage.getItem(`redirect41-${url}`) || url;
  1743.  
  1744. const onPageElement = document.querySelector(`[data-script-namespace][data-script-id="${id || 'null'}"][data-script-name][data-script-version][href]`)
  1745. if (onPageElement && /^https\:\/\/update\.\w+\.org\/scripts\/\d+\/[^.?\/]+\.user\.js$/.test(onPageElement.getAttribute('href') || '')) {
  1746. const result = {
  1747. "id": +onPageElement.getAttribute('data-script-id'),
  1748. // "created_at": "2023-08-24T21:16:50.000Z",
  1749. // "daily_installs": 21,
  1750. // "total_installs": 3310,
  1751. // "code_updated_at": "2023-12-20T07:46:54.000Z",
  1752. // "support_url": null,
  1753. // "fan_score": "74.1",
  1754. "namespace": `${onPageElement.getAttribute('data-script-namespace')}`,
  1755. // "contribution_url": null,
  1756. // "contribution_amount": null,
  1757. // "good_ratings": 11,
  1758. // "ok_ratings": 0,
  1759. // "bad_ratings": 0,
  1760. // "users": [
  1761. // {
  1762. // "id": 371179,
  1763. // "name": "𝖢𝖸 𝖥𝗎𝗇𝗀",
  1764. // "url": "https://greasyfork.org/users/371179-%F0%9D%96%A2%F0%9D%96%B8-%F0%9D%96%A5%F0%9D%97%8E%F0%9D%97%87%F0%9D%97%80"
  1765. // }
  1766. // ],
  1767. "name": `${onPageElement.getAttribute('data-script-name')}`,
  1768. // "description": "Adds various features and improves the Greasy Fork experience",
  1769. // "url": "https://greasyfork.org/scripts/473830-greasy-fork",
  1770. // "code_url": "https://update.greasyfork.org/scripts/473830/Greasy%20Fork%2B%2B.user.js",
  1771. "code_url": `${onPageElement.getAttribute('href')}`,
  1772. // "license": "MIT License",
  1773. "version": `${onPageElement.getAttribute('data-script-version')}`,
  1774. // "locale": "en",
  1775. // "deleted": false
  1776. };
  1777. req.resolve(result);
  1778. return;
  1779. }
  1780. await (networkMP1 = networkMP1.then(() => new Promise(unlock => {
  1781. const maxAgeInSeconds = 900;
  1782. const rd = previousIsCache ? 1 : Math.floor(Math.random() * 80 + 80);
  1783. let fetchStart = 0;
  1784.  
  1785. const fetchOptions = noCache ? {
  1786. method: 'GET',
  1787. cache: 'reload',
  1788. credentials: 'omit',
  1789. headers: {
  1790. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  1791. }
  1792. } : {
  1793. method: 'GET',
  1794. cache: 'force-cache',
  1795. credentials: 'omit',
  1796. headers: {
  1797. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  1798. }
  1799. };
  1800.  
  1801. new Promise(r => setTimeout(r, rd))
  1802. .then(() => {
  1803. fetchStart = Date.now();
  1804. })
  1805. .then(() => DO_CORS ? corsFetch(fetchUrl, fetchOptions): standardFetch(fetchUrl, fetchOptions))
  1806. .then((response) => {
  1807.  
  1808. if (fetchUrl !== response.url) {
  1809. sessionStorage.setItem(`redirect41-${url}`, response.url);
  1810. sessionStorage.setItem(`redirect41-${fetchUrl}`, response.url);
  1811. }
  1812. let fetchStop = Date.now();
  1813. // const dd = fetchStop - fetchStart;
  1814. // dd (cache) = {min: 1, max: 8, avg: 3.7}
  1815. // dd (normal) = {min: 136, max: 316, avg: 162.62}
  1816. // ss.push(dd)
  1817. // ss.maxValue = Math.max(...ss);
  1818. // ss.minValue = Math.min(...ss);
  1819. // ss.avgValue = sum(ss)/ss.length;
  1820. // console.log(dd)
  1821. // console.log(ss)
  1822. previousIsCache = (fetchStop - fetchStart) < (3.7 + 162.62) / 2;
  1823. UU.log(`${response.status}: ${response.url}`)
  1824. // UU.log(response)
  1825. if (response.ok === true) {
  1826. unlock();
  1827. return response.json;
  1828. }
  1829. if (response.status === 503) {
  1830. return new Promise(r => setTimeout(r, 270 + rd)).then(() => {
  1831. unlock();
  1832. return getScriptData(id, true);
  1833. });
  1834. }
  1835. if (response.status === 404) {
  1836. // script XXXX has been reported and is pending review by a moderator.
  1837. unlock();
  1838. return null
  1839. }
  1840. console.warn(response.status, response);
  1841. new Promise(r => setTimeout(r, 470)).then(unlock); // reload later
  1842. })
  1843. .then((data) => req.resolve(data))
  1844. .catch((e) => {
  1845. unlock();
  1846. UU.log(id, url)
  1847. console.warn(e)
  1848. // reject(e)
  1849. })
  1850. })).catch(() => { }))
  1851.  
  1852. });
  1853.  
  1854. }
  1855. const getScriptData = (id, noCache) => {
  1856. if (!(+id > 0)) return Promise.resolve();
  1857. id = +id;
  1858. const reqStores = noCache ? reqStoresB : reqStoresA;
  1859. const cachedReq = reqStores.get(id);
  1860. if (cachedReq) return cachedReq;
  1861. const req = new PromiseExternal();
  1862. reqStores.set(id, req);
  1863. getScriptDataAN(noCache);
  1864. return req;
  1865. }
  1866.  
  1867. /**
  1868. * Get user data from Greasy Fork API
  1869. *
  1870. * @param {string} userID User ID
  1871. * @returns {Promise} User data
  1872. */
  1873. const getUserData = (userID, noCache) => {
  1874. if (!(userID >= 0)) return Promise.resolve()
  1875.  
  1876. const DO_CORS = /^(cn-greasyfork|greasyfork|sleazyfork)\.org$/.test(window.location.hostname) ? `api.${window.location.hostname}` : '';
  1877. const url = `https://${DO_CORS || window.location.hostname}/users/${userID}.json`;
  1878. const fetchUrl = sessionStorage.getItem(`redirect41-${url}`) || url;
  1879. return new Promise((resolve, reject) => {
  1880.  
  1881.  
  1882. networkMP2 = networkMP2.then(() => new Promise(unlock => {
  1883.  
  1884. const maxAgeInSeconds = 900;
  1885. const rd = Math.floor(Math.random() * 80 + 80);
  1886.  
  1887. const fetchOptions = noCache ? {
  1888. method: 'GET',
  1889. cache: 'reload',
  1890. credentials: 'omit',
  1891. headers: {
  1892. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  1893. }
  1894. } : {
  1895. method: 'GET',
  1896. cache: 'force-cache',
  1897. credentials: 'omit',
  1898. headers: {
  1899. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  1900. }
  1901. };
  1902.  
  1903. new Promise(r => setTimeout(r, rd))
  1904.  
  1905. .then(() => DO_CORS ? corsFetch(fetchUrl, fetchOptions) : standardFetch(fetchUrl, fetchOptions))
  1906. .then((response) => {
  1907.  
  1908. if (fetchUrl !== response.url) {
  1909. sessionStorage.setItem(`redirect41-${url}`, response.url);
  1910. sessionStorage.setItem(`redirect41-${fetchUrl}`, response.url);
  1911. }
  1912.  
  1913. UU.log(`${response.status}: ${response.url}`)
  1914. if (response.ok === true) {
  1915. unlock();
  1916. return response.json;
  1917. }
  1918. if (response.status === 503) {
  1919. return new Promise(r => setTimeout(r, 270 + rd)).then(() => {
  1920. unlock();
  1921. return getUserData(userID, true); // reload later
  1922. });
  1923. }
  1924. if (response.status === 404) {
  1925. // user XXXX has been reported and is pending review by a moderator. ????
  1926. unlock();
  1927. return null
  1928. }
  1929. console.warn(response.status, response);
  1930. new Promise(r => setTimeout(r, 470)).then(unlock);
  1931. })
  1932. .then((data) => resolve(data))
  1933. .catch((e) => {
  1934. setTimeout(() => {
  1935. unlock()
  1936. }, 270)
  1937. UU.log(userID, url)
  1938. console.warn(e)
  1939. // reject(e)
  1940. })
  1941.  
  1942.  
  1943.  
  1944. })).catch(() => { })
  1945.  
  1946. });
  1947. }
  1948. const getTotalInstalls = (data) => {
  1949. if (!data || !data.scripts) return;
  1950. return new Promise((resolve, reject) => {
  1951. const totalInstalls = [];
  1952.  
  1953. data.scripts.forEach((element) => {
  1954. totalInstalls.push(parseInt(element.total_installs, 10));
  1955. });
  1956.  
  1957. resolve(totalInstalls.reduce((a, b) => a + b, 0));
  1958. });
  1959. };
  1960.  
  1961.  
  1962. const communicationId = WinComm.newCommunicationId();
  1963. const wincomm = WinComm.createInstance(communicationId);
  1964.  
  1965.  
  1966. const isInstalled = (script) => {
  1967. return new Promise((resolve, reject) => {
  1968.  
  1969. promiseScriptCheck.then(d => {
  1970.  
  1971. if (!d) return null;
  1972.  
  1973. const data = d.data;
  1974. const al = data.type % 10;
  1975. if (al === 0) {
  1976. // no namespace
  1977. resolve([null, script.name, '']);
  1978. } else if (al === 1) {
  1979. // namespace
  1980.  
  1981. if (!script.namespace) {
  1982.  
  1983. getRafPromise() // foreground
  1984. .then(() => getScriptData(script.id))
  1985. .then((script) => {
  1986. resolve([null, script.name, script.namespace]);
  1987. });
  1988.  
  1989. } else {
  1990.  
  1991. resolve([null, script.name, script.namespace]);
  1992. }
  1993.  
  1994. }
  1995.  
  1996.  
  1997. })
  1998.  
  1999.  
  2000. }).then((res) => {
  2001.  
  2002.  
  2003. return new Promise((resolve, reject) => {
  2004.  
  2005. if (!res) return '';
  2006.  
  2007.  
  2008. const [_, name, namespace] = res;
  2009. wincomm.request('installedVersion.req', {
  2010. name,
  2011. namespace
  2012. }).then(d => {
  2013. resolve(d.data.version)
  2014. })
  2015.  
  2016. })
  2017.  
  2018. })
  2019.  
  2020. /*
  2021. const external = unsafeWindow.external;
  2022. const scriptHandler = GM.info.scriptHandler;
  2023. if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey') {
  2024. external.Violentmonkey.isInstalled(name, namespace).then((data) => resolve(data));
  2025. return;
  2026. }
  2027.  
  2028. if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') {
  2029. external.Tampermonkey.isInstalled(name, namespace, (data) => {
  2030. (data.installed) ? resolve(data.version) : resolve();
  2031. });
  2032. return;
  2033. }
  2034. */
  2035.  
  2036.  
  2037. };
  2038.  
  2039. const compareVersions = (v1, v2) => {
  2040. if (!v1 || !v2) return NaN;
  2041. if (v1 === null || v2 === null) return NaN;
  2042. if (v1 === v2) return 0;
  2043.  
  2044. const sv1 = v1.split('.').map((index) => parseInt(index));
  2045. const sv2 = v2.split('.').map((index) => parseInt(index));
  2046.  
  2047. const count = Math.max(sv1.length, sv2.length);
  2048.  
  2049. for (let index = 0; index < count; index++) {
  2050. if (isNaNx(sv1[index]) || isNaNx(sv2[index])) return NaN;
  2051. if (sv1[index] > sv2[index]) return 1;
  2052. if (sv1[index] < sv2[index]) return -1;
  2053. }
  2054.  
  2055. return 0;
  2056. };
  2057.  
  2058.  
  2059. /**
  2060. * Return label for the hide script button
  2061. *
  2062. * @param {boolean} hidden Is hidden
  2063. * @returns {string} Label
  2064. */
  2065. const blockLabel = (hidden) => {
  2066. return hidden ? (locales[lang] ? locales[lang].notHide : locales.en.notHide) : (locales[lang] ? locales[lang].hide : locales.en.hide)
  2067. }
  2068.  
  2069. /**
  2070. * Return label for the install button
  2071. *
  2072. * @param {number} update Update value
  2073. * @returns {string} Label
  2074. */
  2075. const installLabel = (update) => {
  2076. switch (update) {
  2077. case 0: {
  2078. return locales[lang] ? locales[lang].reinstall : locales.en.reinstall
  2079. }
  2080. case 1: {
  2081. return locales[lang] ? locales[lang].update : locales.en.update
  2082. }
  2083. case -1: {
  2084. return locales[lang] ? locales[lang].downgrade : locales.en.downgrade
  2085. }
  2086. default: {
  2087. return locales[lang] ? locales[lang].install : locales.en.install
  2088. }
  2089. }
  2090. }
  2091.  
  2092. const hideBlacklistedDiscussion = (element, list) => {
  2093.  
  2094. const scriptLink = element.querySelector('a.script-link')
  2095. const m = /\/scripts\/(\d+)/.exec(scriptLink);
  2096. const id = m ? +m[1] : 0;
  2097. if (!(id > 0)) return;
  2098.  
  2099. switch (list) {
  2100. case 'hiddenList': {
  2101. const container = element.closest('.discussion-list-container') || element;
  2102. if (hiddenList.indexOf(id) >= 0) {
  2103. container.classList.add('hidden');
  2104. }
  2105. // if (customBlacklist && (customBlacklist.test(name) || customBlacklist.test(description)) && !element.classList.contains('blacklisted')) {
  2106. // element.classList.add('blacklisted', 'custom-blacklist');
  2107. // if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  2108. // let scriptLink = element.querySelector('.script-link');
  2109. // if (scriptLink) { scriptLink.textContent += ' (custom-blacklist)'; }
  2110. // }
  2111. // }
  2112. break;
  2113. }
  2114. default:
  2115. UU.log('No blacklists');
  2116. break;
  2117. }
  2118.  
  2119. }
  2120. const hideBlacklistedScript = (element, list) => {
  2121. if (!element) return;
  2122. const scriptLink = element.querySelector('.script-link')
  2123.  
  2124. const name = scriptLink ? scriptLink.textContent : '';
  2125. const descriptionElem = element.querySelector('.script-description')
  2126. const description = descriptionElem ? descriptionElem.textContent : '';
  2127.  
  2128. if (!name) return;
  2129.  
  2130. switch (list) {
  2131. case 'nonLatins':
  2132. if ((nonLatins.test(name) || nonLatins.test(description)) && !element.classList.contains('blacklisted')) {
  2133. element.classList.add('blacklisted', 'non-latins');
  2134. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  2135. let scriptLink = element.querySelector('.script-link');
  2136. if (scriptLink) { scriptLink.textContent += ' (non-latin)'; }
  2137. }
  2138. }
  2139. break;
  2140. case 'blacklist':
  2141. if (isBlackList(name, description) && !element.classList.contains('blacklisted')) {
  2142. element.classList.add('blacklisted', 'blacklist');
  2143. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  2144. let scriptLink = element.querySelector('.script-link');
  2145. if (scriptLink) { scriptLink.textContent += ' (blacklist)'; }
  2146. }
  2147. }
  2148. break;
  2149. case 'customBlacklist': {
  2150. const customBlacklist = customBlacklistRF;
  2151. if (customBlacklist && (customBlacklist(name) >= 0 || customBlacklist(description) >= 0) && !element.classList.contains('blacklisted')) {
  2152. element.classList.add('blacklisted', 'custom-blacklist');
  2153. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  2154. let scriptLink = element.querySelector('.script-link');
  2155. if (scriptLink) { scriptLink.textContent += ' (custom-blacklist)'; }
  2156. }
  2157. }
  2158. break;
  2159. }
  2160. default:
  2161. UU.log('No blacklists');
  2162. break;
  2163. }
  2164. };
  2165.  
  2166. const hideHiddenScript = (element, id, list) => {
  2167. id = +id;
  2168. if (!(id >= 0)) return;
  2169.  
  2170. const isInHiddenList = () => hiddenList.indexOf(id) !== -1;
  2171. const updateScriptLink = (shouldHide) => {
  2172. if (gmc.get('hideHiddenScript') && gmc.get('debugging')) {
  2173. let scriptLink = element.querySelector('.script-link');
  2174. if (scriptLink) {
  2175. if (shouldHide) {
  2176. scriptLink.innerHTML += ' (hidden)';
  2177. } else {
  2178. scriptLink.innerHTML = scriptLink.innerHTML.replace(' (hidden)', '');
  2179. }
  2180. }
  2181. }
  2182. };
  2183.  
  2184. // Check for initial state and set it
  2185. if (isInHiddenList()) {
  2186. element.classList.add('hidden');
  2187. updateScriptLink(true);
  2188. }
  2189.  
  2190. // Add button to hide the script
  2191. const insertButtonHTML = (selector, html) => {
  2192. const target = element.querySelector(selector);
  2193. if (!target) return;
  2194. let p = document.createElement('template');
  2195. p.innerHTML = html;
  2196. target.parentNode.insertBefore(p.content.firstChild, target.nextSibling);
  2197. };
  2198.  
  2199. const isHidden = element.classList.contains('hidden');
  2200. const blockButtonHTML = `<span class=block-button role=button style-16377>${blockLabel(isHidden)}</span>`;
  2201. const blockButtonHeaderHTML = `<span class=block-button role=button style-77329 style="">${blockLabel(isHidden)}</span>`;
  2202.  
  2203. insertButtonHTML('.badge-js, .badge-css', blockButtonHTML);
  2204. insertButtonHTML('header h2', blockButtonHeaderHTML);
  2205.  
  2206. // Add event listener
  2207. const button = element.querySelector('.block-button');
  2208. if (button) {
  2209. button.addEventListener('click', (event) => {
  2210. event.stopPropagation();
  2211. event.stopImmediatePropagation();
  2212.  
  2213. if (!isInHiddenList()) {
  2214. hiddenList.push(id);
  2215. GM.setValue('hiddenList', hiddenList);
  2216.  
  2217. element.classList.add('hidden');
  2218. updateScriptLink(true);
  2219.  
  2220. } else {
  2221. const index = hiddenList.indexOf(id);
  2222. hiddenList.splice(index, 1);
  2223. GM.setValue('hiddenList', hiddenList);
  2224.  
  2225. element.classList.remove('hidden');
  2226. updateScriptLink(false);
  2227. }
  2228.  
  2229. const blockBtn = element.querySelector('.block-button');
  2230. if (blockBtn) blockBtn.textContent = blockLabel(element.classList.contains('hidden'));
  2231. });
  2232. }
  2233. };
  2234.  
  2235. const insertButtonHTML = (element, selector, html) => {
  2236. const target = element.querySelector(selector);
  2237. if (!target) return;
  2238. let p = document.createElement('template');
  2239. p.innerHTML = html;
  2240. let button = p.content.firstChild
  2241. target.parentNode.insertBefore(button, target.nextSibling);
  2242. return button;
  2243. };
  2244.  
  2245. const addInstallButton = (element, url) => {
  2246. return setupInstallLink(insertButtonHTML(element, '.badge-js, .badge-css', `<a class="install-link" href="${url}" style-54998></a>`));
  2247. };
  2248.  
  2249. async function digestMessage(message, algo) {
  2250. const encoder = new TextEncoder();
  2251. const data = encoder.encode(message);
  2252. const hash = await crypto.subtle.digest(algo, data);
  2253. return hash;
  2254. }
  2255.  
  2256. function qexString(buffer) {
  2257. const byteArray = new Uint8Array(buffer);
  2258. const len = byteArray.length;
  2259. const hexCodes = new Array(len * 2);
  2260. const chars = 'a4b3c5d7e6f9h2t';
  2261. for (let i = 0, j = 0; i < len; i++) {
  2262. const byte = byteArray[i];
  2263. hexCodes[j++] = chars[byte >> 4];
  2264. hexCodes[j++] = chars[byte & 0x0F];
  2265. };
  2266. return hexCodes.join('');
  2267. }
  2268.  
  2269. const encodeFileName = (s) => {
  2270. if (!s || typeof s !== 'string') return s;
  2271. s = s.replace(/[.!~*'"();\/\\?@&=$,#]/g, '-').replace(/\s+/g, ' ');
  2272. return encodeURI(s);
  2273. }
  2274.  
  2275. const isLibraryURLWithVersion = (url) => {
  2276. if (!url || typeof url !== 'string') return;
  2277.  
  2278. if (url.includes('.js?version=')) return true;
  2279.  
  2280. if (/\/scripts\/\d+\/\d+\/[^.!~*'"();\/\\?@&=$,#]+\.js/.test(url)) return true;
  2281. return false;
  2282.  
  2283. }
  2284.  
  2285. const showInstallButton = async (scriptID, element) => {
  2286.  
  2287. await getRafPromise().then();
  2288. // if(document.querySelector(`li[data-script-id="${scriptID}"]`))
  2289. let _baseScript = null;
  2290. if (element.nodeName === 'LI' && element.hasAttribute('data-script-id') && element.getAttribute('data-script-id') === `${scriptID}` && element.getAttribute('data-script-language') === 'js') {
  2291.  
  2292. const version = element.getAttribute('data-script-version') || ''
  2293.  
  2294. let scriptCodeURL = element.getAttribute('data-code-url');
  2295. if (!scriptCodeURL || !isVaildURL(scriptCodeURL)) {
  2296.  
  2297. const name = element.getAttribute('data-script-name') || ''
  2298. // if (!/[^\x00-\x7F]/.test(name)) {
  2299.  
  2300. // const scriptName = useHashedScriptName ? qexString(await digestMessage(`${+scriptID} ${version}`, 'SHA-1')).substring(0, 8) : encodeURI(name);
  2301. // const token = useHashedScriptName ? `${scriptName.substring(0, 2)}${scriptName.substring(scriptName.length - 2, scriptName.length)}` : String.fromCharCode(Date.now() % 26 + 97) + Math.floor(Math.random() * 19861 + 19861).toString(36);
  2302. const scriptFilename = element.getAttribute('data-script-type') === 'library' ? `${encodeFileName(name)}.js` : `${encodeFileName(name)}.user.js`;
  2303. // const scriptFilename = `${scriptName}.user.js`;
  2304.  
  2305. // code_url: `https://${location.hostname}/scripts/${scriptID}-${token}/code/${scriptFilename}`,
  2306. // code_url: `https://update.${location.hostname}/scripts/${scriptID}.user.js`,
  2307. scriptCodeURL = `https://update.${location.hostname}/scripts/${scriptID}/${scriptFilename}`
  2308. }
  2309. _baseScript = {
  2310. id: +scriptID,
  2311. // name: name,
  2312. code_url: scriptCodeURL,
  2313. version: version
  2314. }
  2315. // }
  2316.  
  2317. }
  2318.  
  2319. const baseScript = _baseScript || (await getScriptData(scriptID));
  2320.  
  2321. if ((element.nodeName === 'LI' && element.getAttribute('data-script-type') === 'library') || (baseScript.code_url.includes('.js?version='))) {
  2322.  
  2323. let scriptCodeURL = element.getAttribute('data-code-url');
  2324.  
  2325. if (!scriptCodeURL || !isVaildURL(scriptCodeURL)) {
  2326. const script = baseScript.code_url.includes('.js?version=') ? baseScript : (await getScriptData(scriptID));
  2327. scriptCodeURL = script.code_url;
  2328. }
  2329.  
  2330. if (scriptCodeURL && isLibraryURLWithVersion(scriptCodeURL)) {
  2331.  
  2332.  
  2333. const code_url = fixLibraryCodeURL(scriptCodeURL);
  2334.  
  2335. const button = addInstallButton(element, code_url);
  2336. button.textContent = `Copy URL`;
  2337. button.addEventListener('click', function (evt) {
  2338.  
  2339. const target = (evt || 0).target;
  2340. if (!target) return;
  2341.  
  2342. let a = target.nodeName === 'A' ? target : target.querySelector('a[href]');
  2343.  
  2344. if (!a) return;
  2345. let href = target.getAttribute('href');
  2346. if (!href) return;
  2347.  
  2348. evt.preventDefault();
  2349.  
  2350. copyText(href);
  2351.  
  2352.  
  2353. });
  2354.  
  2355. }
  2356.  
  2357.  
  2358. } else {
  2359.  
  2360.  
  2361. if (!baseScript || !baseScript.code_url || !baseScript.version) return;
  2362. const button = addInstallButton(element, baseScript.code_url);
  2363. button.classList.add('install-status-checking');
  2364. button.textContent = `${installLabel()} ${baseScript.version}`;
  2365. const script = baseScript && baseScript.name && baseScript.namespace ? baseScript : (await getScriptData(scriptID));
  2366. if (!script) return;
  2367.  
  2368. const installed = await isInstalled(script);
  2369. const version = (
  2370. baseScript.version && script.version && compareVersions(baseScript.version, script.version) === 1
  2371. ) ? baseScript.version : script.version;
  2372.  
  2373. const update = compareVersions(version, installed); // NaN 1 -1 0
  2374. const label = installLabel(update);
  2375. button.textContent = `${label} ${version}`;
  2376. button.classList.remove('install-status-checking');
  2377.  
  2378.  
  2379. }
  2380.  
  2381. }
  2382.  
  2383. const updateReqStoresWithElementsOrder = (x) => {
  2384. try {
  2385. const reqStoresA_ = reqStoresA;
  2386. const reqStoresB_ = reqStoresB;
  2387. const order2 = [...reqStoresA_.keys()];
  2388. const order3 = [...reqStoresB_.keys()];
  2389. const orders1 = x;
  2390. const orders = new Set([...orders1, ...order2, ...order3]);
  2391. const reqStoresA2 = new Map();
  2392. const reqStoresB2 = new Map();
  2393. for (const id of orders) {
  2394. const reqA = reqStoresA_.get(id);
  2395. if (reqA) reqStoresA2.set(id, reqA);
  2396. const reqB = reqStoresB_.get(id);
  2397. if (reqB) reqStoresB2.set(id, reqB);
  2398. }
  2399. reqStoresA = reqStoresA2;
  2400. reqStoresB = reqStoresB2;
  2401. reqStoresA_.clear();
  2402. reqStoresB_.clear();
  2403. } catch (e) {
  2404. console.warn(e)
  2405. }
  2406. };
  2407.  
  2408. let lastIdArrString = '';
  2409.  
  2410. const foundScriptList = async (scriptList) => {
  2411.  
  2412. let rid = 0;
  2413. let g = () => {
  2414. if (!scriptList || scriptList.isConnected !== true) return;
  2415.  
  2416. const scriptElements = scriptList.querySelectorAll('li[data-script-id]:not([e8kk])');
  2417.  
  2418. for (const element of scriptElements) {
  2419. element.setAttribute('e8kk', '1');
  2420.  
  2421. const scriptID = +element.getAttribute('data-script-id');
  2422. if (!(scriptID > 0)) continue;
  2423.  
  2424. // blacklisted scripts
  2425. if (gmc.get('nonLatins')) hideBlacklistedScript(element, 'nonLatins');
  2426. if (gmc.get('blacklist')) hideBlacklistedScript(element, 'blacklist');
  2427. if (gmc.get('customBlacklist')) hideBlacklistedScript(element, 'customBlacklist');
  2428.  
  2429. // hidden scripts
  2430. if (gmc.get('hideHiddenScript')) hideHiddenScript(element, scriptID, true);
  2431.  
  2432. // install button
  2433. if (gmc.get('showInstallButton')) {
  2434. showInstallButton(scriptID, element)
  2435. }
  2436. }
  2437.  
  2438. const idArr = [...scriptList.querySelectorAll('li[data-script-id]')].map(e => +e.getAttribute('data-script-id'));
  2439. const idArrString = idArr.join(',');
  2440. if (lastIdArrString !== idArrString) {
  2441. lastIdArrString = idArrString;
  2442. updateReqStoresWithElementsOrder(idArr);
  2443. }
  2444.  
  2445. }
  2446. let f = (entries) => {
  2447. const tid = ++rid
  2448. if (entries && entries.length) requestAnimationFrame(() => {
  2449. if (tid === rid) g();
  2450. });
  2451. }
  2452. let mo = new MutationObserver(f);
  2453. mo.observe(scriptList, { subtree: true, childList: true });
  2454.  
  2455. g();
  2456.  
  2457. }
  2458.  
  2459. const foundDiscussionList = (discussionsList) => {
  2460. targetHiddenRecentDateTime = Date.now() - valHideRecentUsersWithin * 3600000;
  2461.  
  2462. let rid = 0;
  2463. let g = () => {
  2464. if (!discussionsList || discussionsList.isConnected !== true) return;
  2465.  
  2466. const scriptElements = discussionsList.querySelectorAll('.discussion-list-item:not([e8kk])');
  2467.  
  2468. for (const element of scriptElements) {
  2469. element.setAttribute('e8kk', '1');
  2470. // const scriptID = +element.getAttribute('data-script-id');
  2471. // if (!(scriptID > 0)) continue;
  2472.  
  2473.  
  2474. // blacklisted scripts
  2475. // if (gmc.get('nonLatins')) hideBlacklistedDiscussion(element, 'nonLatins');
  2476. // if (gmc.get('blacklist')) hideBlacklistedDiscussion(element, 'blacklist');
  2477. if (gmc.get('hideHiddenScript')) hideBlacklistedDiscussion(element, 'hiddenList');
  2478.  
  2479. // // hidden scripts
  2480. // if (gmc.get('hideHiddenScript')) hideBlacklistedDiscussion(element, scriptID, true);
  2481.  
  2482. // // install button
  2483. // if (gmc.get('showInstallButton')) {
  2484. // showInstallButton(scriptID, element)
  2485. // }
  2486. let t;
  2487. if (t = element.querySelector('a.user-link[href*="/users/"]')) {
  2488. const m = /\/users\/(\d+)/.exec(`${t.getAttribute('href')}`);
  2489. if (m) {
  2490. const userId = +m[1];
  2491. mightHideDiscussionByRecentlyNewUser(userId).then((isNewUser)=>{
  2492. element.classList.toggle('discussion-item-by-recent-user', isNewUser);
  2493. });
  2494. }
  2495. }
  2496.  
  2497. }
  2498.  
  2499. }
  2500. let f = (entries) => {
  2501. const tid = ++rid
  2502. if (entries && entries.length) requestAnimationFrame(() => {
  2503. if (tid === rid) g();
  2504. });
  2505. }
  2506. let mo = new MutationObserver(f);
  2507. mo.observe(discussionsList, { subtree: true, childList: true });
  2508.  
  2509. g();
  2510.  
  2511. }
  2512.  
  2513. let promiseScriptCheckResolve = null;
  2514. const promiseScriptCheck = new Promise(resolve => {
  2515. promiseScriptCheckResolve = resolve
  2516. });
  2517.  
  2518. const milestoneNotificationFn = async (o) => {
  2519.  
  2520. const { userLink, userID } = o;
  2521.  
  2522.  
  2523. const milestones = gmc.get('milestoneNotification').replace(/\s/g, '').split(',').map(Number);
  2524.  
  2525. if (!userID) return;
  2526.  
  2527. await new Promise(resolve => setTimeout(resolve, 800)); // delay for reducing server burden
  2528. await new Promise(resolve => requestAnimationFrame(resolve)); // foreground
  2529.  
  2530. const userData = await getUserData(+userID.match(/\d+(?=\D)/g));
  2531. if (!userData) return;
  2532.  
  2533. const [totalInstalls, lastMilestone] = await Promise.all([
  2534. getTotalInstalls(userData),
  2535. GM.getValue('lastMilestone', 0)]);
  2536.  
  2537. const milestone = milestones.filter(milestone => totalInstalls >= milestone).pop();
  2538.  
  2539. UU.log(`total installs are "${totalInstalls}", milestone reached is "${milestone}", last milestone reached is "${lastMilestone}"`);
  2540.  
  2541. if (milestone <= lastMilestone) return;
  2542.  
  2543. if (milestone && milestone >= 0) {
  2544.  
  2545.  
  2546. GM.setValue('lastMilestone', milestone);
  2547.  
  2548. const lang = document.documentElement.lang;
  2549. const text = (locales[lang] ? locales[lang].milestone : locales.en.milestone).replace('$1', milestone.toLocaleString());
  2550.  
  2551. if (typeof GM.notification === 'function') {
  2552. GM.notification({
  2553. text,
  2554. title: GM.info.script.name,
  2555. image: logo,
  2556. onclick: () => {
  2557. window.location = `https://${window.location.hostname}${userID}#user-script-list-section`;
  2558. }
  2559. });
  2560. } else {
  2561. UU.alert(text);
  2562. }
  2563.  
  2564. }
  2565.  
  2566. }
  2567. const onReady = async () => {
  2568.  
  2569. try {
  2570.  
  2571. const gminfo = GM.info || 0;
  2572. if (gminfo) {
  2573.  
  2574. const gminfoscript = gminfo.script || 0;
  2575.  
  2576.  
  2577. const scriptHandlerObject = {
  2578. scriptHandler: gminfo.scriptHandler || '',
  2579. scriptName: gminfoscript.name || '', // not name_i18n
  2580. scriptVersion: gminfoscript.version || '',
  2581. scriptNamespace: gminfoscript.namespace || '',
  2582. communicationId
  2583. };
  2584.  
  2585.  
  2586. wincomm.hook('_$GreasyFork$Msg$OnScriptInstallFeedback',
  2587. {
  2588.  
  2589. ready: (d, evt) => promiseScriptCheckResolve(d),
  2590. userScriptManagerNotDetected: (d, evt) => promiseScriptCheckResolve(null),
  2591. 'installedVersion.res': wincomm.handleResponse
  2592.  
  2593.  
  2594. })
  2595.  
  2596.  
  2597. document.head.appendChild(document.createElement('script')).textContent = `;(${mWindow.contentScriptText})(${JSON.stringify(scriptHandlerObject)}, ${WinComm.createInstance});`;
  2598.  
  2599.  
  2600. }
  2601.  
  2602. addSettingsToMenu();
  2603.  
  2604. setTimeout(() => {
  2605. getRafPromise().then(() => {
  2606. let installBtn = document.querySelector('a[data-script-id][data-script-version]')
  2607. let scriptID = installBtn && installBtn.textContent ? +installBtn.getAttribute('data-script-id') : 0;
  2608. if (scriptID > 0) {
  2609. getScriptData(scriptID, true);
  2610. } else {
  2611. const userLink = document.querySelector('#site-nav .user-profile-link a[href]');
  2612. let userID = userLink ? userLink.getAttribute('href') : '';
  2613. userID = userID ? /users\/(\d+)/.exec(userID) : null;
  2614. if (userID) userID = userID[1];
  2615. if (userID) {
  2616. userID = +userID;
  2617. if (userID > 0) {
  2618. getUserData(userID, true);
  2619. }
  2620. }
  2621. }
  2622. });
  2623. }, 740);
  2624.  
  2625. const userLink = document.querySelector('.user-profile-link a[href]');
  2626. const userID = userLink ? userLink.getAttribute('href') : undefined;
  2627.  
  2628. const urlMatch = (url1, url2) => {
  2629. url1 = `${url1}`
  2630. url2 = `${url2}`;
  2631. if (url1.includes(location.hostname)) {
  2632. url1 = url1.replace(`https://${location.hostname}/`, '/')
  2633. url1 = url1.replace(`http://${location.hostname}/`, '/')
  2634. url1 = url1.replace(/^\/+/, '/')
  2635. } else if (!url1.startsWith('/')) {
  2636. url1 = `/${url1}`;
  2637. }
  2638. if (url2.includes(location.hostname)) {
  2639. url2 = url2.replace(`https://${location.hostname}/`, '/')
  2640. url2 = url2.replace(`http://${location.hostname}/`, '/')
  2641. url2 = url2.replace(/^\/+/, '/')
  2642. } else if (!url2.startsWith('/')) {
  2643. url2 = `/${url2}`;
  2644. }
  2645. url1 = url1.replace(/\?\w+=\w+(&\w+=\w+)*$/, '');
  2646. url2 = url2.replace(/\?\w+=\w+(&\w+=\w+)*$/, '');
  2647. return url1.toLowerCase() === url2.toLowerCase();
  2648. }
  2649.  
  2650. UU.addStyle(mWindow.pageCSS);
  2651. // blacklisted scripts / hidden scripts / install button
  2652. if (!urlMatch(window.location.pathname, userID) && !/discussions/.test(window.location.pathname) && (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript') || gmc.get('showInstallButton'))) {
  2653.  
  2654. const scriptList = document.querySelector('.script-list');
  2655. if (scriptList) {
  2656. foundScriptList(scriptList);
  2657. } else {
  2658. const timeout = Date.now() + 3000;
  2659. /** @type {MutationObserver | null} */
  2660. let mo = null;
  2661. const mutationCallbackForScriptList = () => {
  2662. if (!mo) return;
  2663. const scriptList = document.querySelector('.script-list');
  2664. if (scriptList) {
  2665. mo.disconnect();
  2666. mo.takeRecords();
  2667. mo = null;
  2668. foundScriptList(scriptList);
  2669. } else if (Date.now() > timeout) {
  2670. mo.disconnect();
  2671. mo.takeRecords();
  2672. mo = null;
  2673. }
  2674. }
  2675. mo = new MutationObserver(mutationCallbackForScriptList);
  2676. mo.observe(document, { subtree: true, childList: true });
  2677. }
  2678.  
  2679.  
  2680. // hidden scripts on details page
  2681. const installLinkElement = document.querySelector('#script-info .install-link[data-script-id]');
  2682. setupInstallLink(installLinkElement);
  2683. if (gmc.get('hideHiddenScript') && installLinkElement) {
  2684. const id = +installLinkElement.getAttribute('data-script-id');
  2685. hideHiddenScript(document.querySelector('#script-info'), id, false);
  2686. }
  2687.  
  2688. // add options and style for blacklisted/hidden scripts
  2689. if (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript')) {
  2690. addOptions();
  2691. }
  2692.  
  2693. if (installLinkElement && location.pathname.includes('/scripts/')) {
  2694.  
  2695. installLinkElement.addEventListener('click', async function (e) {
  2696. if (e && e.isTrusted && location.pathname.includes('/scripts/')) {
  2697.  
  2698. await new Promise(r => setTimeout(r, 800));
  2699. await new Promise(r => window.requestAnimationFrame(r));
  2700. await new Promise(r => setTimeout(r, 100));
  2701. // let ethicalads497 = 'ethicalads' in window ? window.ethicalads : undefined;
  2702. // window.ethicalads = { wait: new Promise() }
  2703. document.dispatchEvent(new Event("DOMContentLoaded"));
  2704. document.documentElement.dispatchEvent(new Event("turbo:load"));
  2705. // if (ethicalads497 === undefined) delete window.ethicalads; else window.ethicalads = ethicalads497;
  2706. }
  2707. })
  2708. }
  2709. } else if (/\/discussions/.test(window.location.pathname)) {
  2710.  
  2711. const discussionsList = document.querySelector('.discussion-list');
  2712.  
  2713.  
  2714. if (discussionsList) {
  2715. foundDiscussionList(discussionsList);
  2716. } else {
  2717. const timeout = Date.now() + 3000;
  2718. /** @type {MutationObserver | null} */
  2719. let mo = null;
  2720. const mutationCallbackForScriptList = () => {
  2721. if (!mo) return;
  2722. const discussionsList = document.querySelector('.script-list');
  2723. if (discussionsList) {
  2724. mo.disconnect();
  2725. mo.takeRecords();
  2726. mo = null;
  2727. foundDiscussionList(discussionsList);
  2728. } else if (Date.now() > timeout) {
  2729. mo.disconnect();
  2730. mo.takeRecords();
  2731. mo = null;
  2732. }
  2733. }
  2734. mo = new MutationObserver(mutationCallbackForScriptList);
  2735. mo.observe(document, { subtree: true, childList: true });
  2736. }
  2737. }
  2738.  
  2739. // total installs
  2740. if (gmc.get('showTotalInstalls') && document.querySelector('#user-script-list')) {
  2741. const dailyInstalls = [];
  2742. const totalInstalls = [];
  2743.  
  2744. const dailyInstallElements = document.querySelectorAll('#user-script-list li dd.script-list-daily-installs');
  2745. for (const element of dailyInstallElements) {
  2746. dailyInstalls.push(parseInt(element.textContent.replace(/\D/g, ''), 10));
  2747. }
  2748.  
  2749. const totalInstallElements = document.querySelectorAll('#user-script-list li dd.script-list-total-installs');
  2750. for (const element of totalInstallElements) {
  2751. totalInstalls.push(parseInt(element.textContent.replace(/\D/g, ''), 10));
  2752. }
  2753.  
  2754. const dailyInstallsSum = dailyInstalls.reduce((a, b) => a + b, 0);
  2755. const totalInstallsSum = totalInstalls.reduce((a, b) => a + b, 0);
  2756.  
  2757. const convertLi = (li) => {
  2758.  
  2759. if (!li) return null;
  2760. const a = li.firstElementChild
  2761. if (a === null) return li;
  2762. if (a === li.lastElementChild && a.nodeName === 'A') return a;
  2763.  
  2764.  
  2765. return null;
  2766. }
  2767.  
  2768. const plusSign = document.querySelector('#user-script-list-section a[rel="next"][href*="page="], #user-script-list-section a[rel="prev"][href*="page="]') ? '+' : '';
  2769.  
  2770. const dailyOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(1)'));
  2771. dailyOption && dailyOption.insertAdjacentHTML('beforeend', `<span> (${dailyInstallsSum.toLocaleString()}${plusSign})</span>`);
  2772.  
  2773. const totalOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(2)'));
  2774. totalOption && totalOption.insertAdjacentHTML('beforeend', `<span> (${totalInstallsSum.toLocaleString()}${plusSign})</span>`);
  2775. }
  2776.  
  2777. // milestone notification
  2778. if (gmc.get('milestoneNotification')) {
  2779. milestoneNotificationFn({ userLink, userID });
  2780. }
  2781.  
  2782. if (isScriptFirstUse) GM.setValue('firstUse', false).then(() => {
  2783. gmc.open();
  2784. });
  2785.  
  2786. if (fixLibraryScriptCodeLink) {
  2787.  
  2788.  
  2789. let xpath = "//code[contains(text(), '.js?version=') or contains(text(), '// @require https://')]";
  2790. let snapshot = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
  2791.  
  2792. for (let i = 0; i < snapshot.snapshotLength; i++) {
  2793. let element = snapshot.snapshotItem(i);
  2794. if (element.firstElementChild) continue;
  2795. element.textContent = element.textContent.replace(/\bhttps:\/\/(cn-greasyfork|greasyfork|sleazyfork)\.org\/scripts\/\d+\-[^\/]+\/code\/[^\.]+\.js\?version=\d+\b/, (_) => {
  2796. return fixLibraryCodeURL(_);
  2797. });
  2798. element.parentNode.insertBefore(document.createTextNode('\u200B'), element);
  2799. element.style.display = 'inline-flex';
  2800. setClickToSelect(element);
  2801. }
  2802.  
  2803.  
  2804. }
  2805.  
  2806.  
  2807.  
  2808.  
  2809. if (addAdditionInfoLengthHint && location.pathname.includes('/scripts/') && location.pathname.includes('/versions')) {
  2810.  
  2811. function contentLength(text) {
  2812. return text.replace(/\n/g, ' ').length;
  2813. }
  2814. function contentLengthMax() {
  2815. return 50000;
  2816. }
  2817. let _spanContent = null;
  2818. function updateText(ainfo, span) {
  2819. const value = ainfo.value;
  2820. if (typeof value !== 'string') return;
  2821.  
  2822. if (_spanContent !== value) {
  2823. _spanContent = value;
  2824. span.textContent = `Text Length: ${contentLength(value)} / ${contentLengthMax()}`;
  2825.  
  2826.  
  2827. }
  2828. }
  2829. function onChange(evt) {
  2830. let ainfo = (evt || 0).target;
  2831. if (!ainfo) return;
  2832. let span = ainfo.parentNode.querySelector('.script-version-ainfo-span');
  2833. if (!span) return;
  2834.  
  2835. updateText(ainfo, span);
  2836.  
  2837. }
  2838. function kbEvent(evt) {
  2839. Promise.resolve().then(() => {
  2840. onChange(evt);
  2841.  
  2842. })
  2843. }
  2844. for (const ainfo of document.querySelectorAll('textarea[id^="script-version-additional-info"]')) {
  2845. let span = document.createElement('span');
  2846. span.classList.add('script-version-ainfo-span');
  2847. ainfo.addEventListener('change', onChange, false);
  2848. ainfo.addEventListener('keydown', kbEvent, false);
  2849. ainfo.addEventListener('keypress', kbEvent, false);
  2850. ainfo.addEventListener('keyup', kbEvent, false);
  2851. updateText(ainfo, span);
  2852. ainfo.parentNode.insertBefore(span, ainfo.nextSibling);
  2853.  
  2854.  
  2855. }
  2856.  
  2857.  
  2858. }
  2859.  
  2860. } catch (e) {
  2861. console.log(e);
  2862. }
  2863.  
  2864.  
  2865.  
  2866. }
  2867.  
  2868.  
  2869.  
  2870.  
  2871. Promise.resolve().then(() => {
  2872. if (document.readyState !== 'loading') {
  2873. onReady();
  2874. } else {
  2875. window.addEventListener("DOMContentLoaded", onReady, false);
  2876. }
  2877. });
  2878.  
  2879. })();