Greasy Fork++

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

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

  1. // ==UserScript==
  2. // @name Greasy Fork++
  3. // @namespace https://github.com/iFelix18
  4. // @version 3.2.2
  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.3.0/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. // @connect greasyfork.org
  25. // @compatible chrome
  26. // @compatible edge
  27. // @compatible firefox
  28. // @compatible safari
  29. // @compatible brave
  30. // @grant GM.deleteValue
  31. // @grant GM.getValue
  32. // @grant GM.notification
  33. // @grant GM.registerMenuCommand
  34. // @grant GM.setValue
  35. // @grant unsafeWindow
  36. // @run-at document-start
  37. // @inject-into content
  38. // ==/UserScript==
  39.  
  40. /* global GM_config, VM, GM, WinComm */
  41.  
  42. /**
  43. * @typedef { typeof import("./library/WinComm.js") } WinComm
  44. */
  45.  
  46. /** @type {WinComm} */
  47. const WinComm = this.WinComm;
  48.  
  49. // -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js --------
  50. // optimized by CY Fung to remove $ dependency and observe creation
  51. const UU = (function () {
  52. const scriptName = GM.info.script.name;
  53. const scriptVersion = GM.info.script.version;
  54. const authorMatch = /^(.*?)\s<\S[^\s@]*@\S[^\s.]*\.\S+>$/.exec(GM.info.script.author);
  55. const author = authorMatch ? authorMatch[1] : GM.info.script.author;
  56. let scriptId = scriptName.toLowerCase().replace(/\s/g, "-");
  57. let loggingEnabled = false;
  58.  
  59. const log = (message) => {
  60. if (loggingEnabled) {
  61. console.log(`${scriptName}:`, message);
  62. }
  63. };
  64.  
  65. const error = (message) => {
  66. console.error(`${scriptName}:`, message);
  67. };
  68.  
  69. const warn = (message) => {
  70. console.warn(`${scriptName}:`, message);
  71. };
  72.  
  73. const alert = (message) => {
  74. window.alert(`${scriptName}: ${message}`);
  75. };
  76.  
  77. /** @param {string} text */
  78. const short = (text, length) => {
  79. const s = text.split(" ");
  80. const l = Number(length);
  81. return s.length > l
  82. ? `${s.slice(0, l).join(" ")} [...]`
  83. : text;
  84. };
  85.  
  86. const addStyle = (css) => {
  87. const head = document.head || document.querySelector("head");
  88. const style = document.createElement("style");
  89. style.textContent = css;
  90. head.appendChild(style);
  91. };
  92.  
  93. const init = async (options = {}) => {
  94. scriptId = options.id || scriptId;
  95. loggingEnabled = typeof options.logging === "boolean" ? options.logging : false;
  96. console.info(
  97. `%c${scriptName}\n%cv${scriptVersion}${author ? ` by ${author}` : ""} is running!`,
  98. "color:red;font-weight:700;font-size:18px;text-transform:uppercase",
  99. ""
  100. );
  101. };
  102.  
  103. return {
  104. init,
  105. log,
  106. error,
  107. warn,
  108. alert,
  109. short,
  110. addStyle
  111. };
  112. })();
  113.  
  114. // -------- UU Fucntion - original code: https://fastly.jsdelivr.net/npm/@ifelix18/utils@6.5.0/lib/index.min.js --------
  115.  
  116.  
  117. const mWindow = (() => {
  118.  
  119.  
  120. const fields = {
  121. hideBlacklistedScripts: {
  122. 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>',
  123. section: ['Features'],
  124. labelPos: 'right',
  125. type: 'checkbox',
  126. default: true
  127. },
  128. hideHiddenScript: {
  129. 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',
  130. labelPos: 'right',
  131. type: 'checkbox',
  132. default: true
  133. },
  134. showInstallButton: {
  135. label: 'Install button:<br><span>Add to the scripts list a button to install the script directly</span>',
  136. labelPos: 'right',
  137. type: 'checkbox',
  138. default: true
  139. },
  140. showTotalInstalls: {
  141. label: 'Installations:<br><span>Shows the number of daily and total installations on the user profile</span>',
  142. labelPos: 'right',
  143. type: 'checkbox',
  144. default: true
  145. },
  146. milestoneNotification: {
  147. 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>',
  148. labelPos: 'left',
  149. type: 'text',
  150. title: 'Separate milestones with a comma!',
  151. size: 150,
  152. default: '10, 100, 500, 1000, 2500, 5000, 10000, 100000, 1000000'
  153. },
  154. nonLatins: {
  155. label: 'Non-Latin:<br><span>This list blocks all scripts with non-Latin characters in the title/description</span>',
  156. section: ['Lists'],
  157. labelPos: 'right',
  158. type: 'checkbox',
  159. default: false // not true
  160. },
  161. blacklist: {
  162. label: 'Blacklist:<br><span>A "non-opinionable" list that blocks all scripts with emoji in the title/description, references to "bots", "cheats" and some online game sites, and other "bullshit"</span>',
  163. labelPos: 'right',
  164. type: 'checkbox',
  165. default: true
  166. },
  167. customBlacklist: {
  168. 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>',
  169. labelPos: 'left',
  170. type: 'text',
  171. title: 'Separate unwanted words with a comma!',
  172. size: 150,
  173. default: ''
  174. },
  175. hiddenList: {
  176. label: 'Hidden Scripts:<br><span>Block individual undesired scripts by their unique IDs<br>Separate IDs with a comma</span>',
  177. labelPos: 'left',
  178. type: 'textarea',
  179. title: 'Separate IDs with a comma!',
  180. default: '',
  181. save: false
  182. },
  183. logging: {
  184. label: 'Logging',
  185. section: ['Developer options'],
  186. labelPos: 'right',
  187. type: 'checkbox',
  188. default: false
  189. },
  190. debugging: {
  191. label: 'Debugging',
  192. labelPos: 'right',
  193. type: 'checkbox',
  194. default: false
  195. }
  196. }
  197.  
  198. 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='
  199.  
  200. const locales = { /* cSpell: disable */
  201. de: {
  202. downgrade: 'Auf zurückstufen',
  203. hide: '❌ Dieses skript ausblenden',
  204. install: 'Installieren',
  205. notHide: '✔️ Dieses skript nicht ausblenden',
  206. milestone: 'Herzlichen Glückwunsch, Ihre Skripte haben den Meilenstein von insgesamt $1 Installationen überschritten!',
  207. reinstall: 'Erneut installieren',
  208. update: 'Auf aktualisieren'
  209. },
  210. en: {
  211. downgrade: 'Downgrade to',
  212. hide: '❌ Hide this script',
  213. install: 'Install',
  214. notHide: '✔️ Not hide this script',
  215. milestone: 'Congrats, your scripts got over the milestone of $1 total installs!',
  216. reinstall: 'Reinstall',
  217. update: 'Update to'
  218. },
  219. es: {
  220. downgrade: 'Degradar a',
  221. hide: '❌ Ocultar este script',
  222. install: 'Instalar',
  223. notHide: '✔️ No ocultar este script',
  224. milestone: '¡Felicidades, sus scripts superaron el hito de $1 instalaciones totales!',
  225. reinstall: 'Reinstalar',
  226. update: 'Actualizar a'
  227. },
  228. fr: {
  229. downgrade: 'Revenir à',
  230. hide: '❌ Cacher ce script',
  231. install: 'Installer',
  232. notHide: '✔️ Ne pas cacher ce script',
  233. milestone: 'Félicitations, vos scripts ont franchi le cap des $1 installations au total!',
  234. reinstall: 'Réinstaller',
  235. update: 'Mettre à'
  236. },
  237. it: {
  238. downgrade: 'Riporta a',
  239. hide: '❌ Nascondi questo script',
  240. install: 'Installa',
  241. notHide: '✔️ Non nascondere questo script',
  242. milestone: 'Congratulazioni, i tuoi script hanno superato il traguardo di $1 installazioni totali!',
  243. reinstall: 'Reinstalla',
  244. update: 'Aggiorna a'
  245. },
  246. ru: {
  247. downgrade: 'Откатить до',
  248. hide: '❌ Скрыть этот скрипт',
  249. install: 'Установить',
  250. notHide: '✔️ Не скрывать этот сценарий',
  251. milestone: 'Поздравляем, ваши скрипты преодолели рубеж в $1 установок!',
  252. reinstall: 'Переустановить',
  253. update: 'Обновить до'
  254. },
  255. 'zh-CN': {
  256. downgrade: '降级到',
  257. hide: '❌ 隐藏此脚本',
  258. install: '安装',
  259. notHide: '✔️ 不隐藏此脚本',
  260. milestone: '恭喜,您的脚本超过了 $1 次总安装的里程碑!',
  261. reinstall: '重新安装',
  262. update: '更新到'
  263. },
  264. 'zh-TW': {
  265. downgrade: '降級至',
  266. hide: '❌ 隱藏此腳本',
  267. install: '安裝',
  268. notHide: '✔️ 不隱藏此腳本',
  269. milestone: '恭喜,您的腳本安裝總數已超過 $1!',
  270. reinstall: '重新安裝',
  271. update: '更新至'
  272. },
  273. 'ja': {
  274. downgrade: 'ダウングレードする',
  275. hide: '❌ このスクリプトを隠す',
  276. install: 'インストール',
  277. notHide: '✔️ このスクリプトを隠さない',
  278. milestone: 'おめでとうございます、あなたのスクリプトの合計インストール回数が $1 を超えました!',
  279. reinstall: '再インストール',
  280. update: '更新する'
  281. },
  282. 'ko': {
  283. downgrade: '다운그레이드하기',
  284. hide: '❌ 이 스크립트 숨기기',
  285. install: '설치',
  286. notHide: '✔️ 이 스크립트 숨기지 않기',
  287. milestone: '축하합니다, 스크립트의 총 설치 횟수가 $1을 넘었습니다!',
  288. reinstall: '재설치',
  289. update: '업데이트하기'
  290. }
  291.  
  292. };
  293.  
  294. const blacklist = [ /* cSpell: disable-next-line */
  295. '\\bagar((\\.)?io)?\\b', '\\bagma((\\.)?io)?\\b', '\\baimbot\\b', '\\barras((\\.)?io)?\\b', '\\bbot(s)?\\b', '\\bbubble((\\.)?am)?\\b', '\\bcheat(s)?\\b', '\\bdiep((\\.)?io)?\\b', '\\bfreebitco((\\.)?in)?\\b', '\\bgota((\\.)?io)?\\b', '\\bhack(s)?\\b', '\\bkrunker((\\.)?io)?\\b', '\\blostworld((\\.)?io)?\\b', '\\bmoomoo((\\.)?io)?\\b', '\\broblox(\\.com)?\\b', '\\bshell\\sshockers\\b', '\\bshellshock((\\.)?io)?\\b', '\\bshellshockers\\b', '\\bskribbl((\\.)?io)?\\b', '\\bslither((\\.)?io)?\\b', '\\bsurviv((\\.)?io)?\\b', '\\btaming((\\.)?io)?\\b', '\\bvenge((\\.)?io)?\\b', '\\bvertix((\\.)?io)?\\b', '\\bzombs((\\.)?io)?\\b', '\\p{Extended_Pictographic}'
  296. ];
  297.  
  298.  
  299. const settingsCSS = `
  300. /*
  301. #greasyfork-plus label::before {
  302. content:'';
  303. display:block;
  304. position:absolute;
  305. left:0;
  306. right:0;
  307. top:0;
  308. bottom:0;
  309. z-index:1;
  310. }
  311. #greasyfork-plus label {
  312. position:relative;
  313. z-index:0;
  314. }
  315. */
  316. html {
  317. color: #222;
  318. background: #f9f9f9;
  319. }
  320. #greasyfork-plus{
  321. --config-var-display: flex;
  322. }
  323. #greasyfork-plus * {
  324. font-family:Open Sans,sans-serif,Segoe UI Emoji !important;
  325. font-size:12px
  326. }
  327. #greasyfork-plus .section_header[class] {
  328. background-color:#670000;
  329. background-image:linear-gradient(#670000,#900);
  330. border:1px solid transparent;
  331. color:#fff
  332. }
  333. #greasyfork-plus .field_label[class]{
  334. margin-bottom:4px
  335. }
  336. #greasyfork-plus .field_label[class] span{
  337. font-size:95%;
  338. font-style:italic;
  339. opacity:.8;
  340. }
  341. #greasyfork-plus .field_label[class] b{
  342. color:#670000
  343. }
  344. #greasyfork-plus_logging_var[class],
  345. #greasyfork-plus_debugging_var[class] {
  346. --config-var-display: inline-flex;
  347. }
  348. #greasyfork-plus #greasyfork-plus_logging_var label.field_label[class],
  349. #greasyfork-plus #greasyfork-plus_debugging_var label.field_label[class] {
  350. margin-bottom:0;
  351. align-self: center;
  352. }
  353. #greasyfork-plus .config_var[class]{
  354. display:var(--config-var-display);
  355. position: relative;
  356. }
  357. #greasyfork-plus_customBlacklist_var[class],
  358. #greasyfork-plus_hiddenList_var[class],
  359. #greasyfork-plus_milestoneNotification_var[class]{
  360. flex-direction:column;
  361. margin-left:21px;
  362. }
  363. #greasyfork-plus_customBlacklist_var[class]::before,
  364. #greasyfork-plus_hiddenList_var[class]::before,
  365. #greasyfork-plus_milestoneNotification_var[class]::before{
  366. /* content: "◉"; */
  367. content: "◎";
  368. position: absolute;
  369. left: auto;
  370. top: auto;
  371. margin-left: -16px;
  372. }
  373. #greasyfork-plus_field_customBlacklist[class],
  374. #greasyfork-plus_field_milestoneNotification[class]{
  375. flex:1;
  376. }
  377. #greasyfork-plus_field_hiddenList[class]{
  378. box-sizing:border-box;
  379. overflow:hidden;
  380. resize:none;
  381. width:100%
  382. }
  383. body > #greasyfork-plus_wrapper:only-child {
  384. box-sizing: border-box;
  385. overflow: auto;
  386. max-height: calc(100vh - 72px);
  387. padding: 12px;
  388. /* overflow: auto; */
  389. scrollbar-gutter: both-edges;
  390. background: rgba(127,127,127,0.05);
  391. border: 1px solid rgba(127,127,127,0.5);
  392. }
  393. #greasyfork-plus_wrapper > #greasyfork-plus_buttons_holder:last-child {
  394. position: fixed;
  395. bottom: 0;
  396. right: 0;
  397. margin: 0 12px 6px 0;
  398. }
  399. #greasyfork-plus .saveclose_buttons[class] {
  400. padding: 4px 14px;
  401. margin: 6px;
  402. }
  403. #greasyfork-plus .section_header_holder#greasyfork-plus_section_2[class] {
  404. position: fixed;
  405. left: 0;
  406. bottom: 0;
  407. margin: 8px;
  408. }
  409. #greasyfork-plus .section_header#greasyfork-plus_section_header_2[class] {
  410. background: #000;
  411. color: #eee;
  412. }
  413. #greasyfork-plus_header[class]{
  414. font-size: 16pt;
  415. font-weight: bold;
  416. }
  417. `;
  418.  
  419. const pageCSS = `
  420. .script-list li.blacklisted{
  421. display:none;
  422. background:#321919;
  423. color:#e8e6e3
  424. }
  425. .script-list li.hidden{
  426. display:none;
  427. background:#321932;
  428. color:#e8e6e3
  429. }
  430. .script-list li.blacklisted a:not(.install-link),.script-list li.hidden a:not(.install-link){
  431. color:#ff8484
  432. }
  433. #script-info.hidden,#script-info.hidden .user-content{
  434. background:#321932;
  435. color:#e8e6e3
  436. }
  437. #script-info.hidden a:not(.install-link):not(.install-help-link){
  438. color:#ff8484
  439. }
  440. #script-info.hidden code{
  441. background-color:transparent
  442. }
  443. html {
  444. --block-btn-color:#111;
  445. --block-btn-bgcolor:#eee;
  446. }
  447. #script-info.hidden, #script-info.hidden .user-content {
  448. --block-btn-color:#eee;
  449. --block-btn-bgcolor:#111;
  450. }
  451. [style-54998]{
  452. float:right;
  453. font-size: 70%;
  454. text-decoration:none;
  455. }
  456. [style-16377]{
  457. cursor:pointer;
  458. font-size:70%;
  459. white-space:nowrap;
  460. border: 1px solid #888;
  461. background: var(--block-btn-bgcolor, #eee);
  462. color: var(--block-btn-color);
  463. border-radius: 4px;
  464. padding: 0px 6px;
  465. margin: 0 8px;
  466. }
  467. [style-77329] {
  468. cursor: pointer;
  469. margin-left: 1ex;
  470. white-space: nowrap;
  471. float: right;
  472. border: 1px solid #888;
  473. background: var(--block-btn-bgcolor, #eee);
  474. color: var(--block-btn-color);
  475. border-radius: 4px;
  476. padding: 0px 6px;
  477. }
  478. a#hyperlink-35389,
  479. a#hyperlink-40361,
  480. a#hyperlink-35389:visited,
  481. a#hyperlink-40361:visited,
  482. a#hyperlink-35389:hover,
  483. a#hyperlink-40361:hover,
  484. a#hyperlink-35389:focus,
  485. a#hyperlink-40361:focus,
  486. a#hyperlink-35389:active,
  487. a#hyperlink-40361:active {
  488. border: none !important;
  489. outline: none !important;
  490. box-shadow: none !important;
  491. appearance: none !important;
  492. background: none !important;
  493. color:inherit !important;
  494. }
  495. a#hyperlink-35389{
  496. opacity: var(--hyperlink-blacklisted-option-opacity);
  497. }
  498. a#hyperlink-40361{
  499. opacity: var(--hyperlink-hidden-option-opacity);
  500. }
  501. html {
  502. --hyperlink-blacklisted-option-opacity: 0.5;
  503. --hyperlink-hidden-option-opacity: 0.5;
  504. }
  505. .list-option.list-current[class] > a[href] {
  506. text-decoration:none;
  507. }
  508. html {
  509. --blacklisted-display: none;
  510. --hidden-display: none;
  511. }
  512. [blacklisted-shown] {
  513. --blacklisted-display: list-item;
  514. --hyperlink-blacklisted-option-opacity: 1;
  515. }
  516. [hidden-shown] {
  517. --hidden-display: list-item;
  518. --hyperlink-hidden-option-opacity: 1;
  519. }
  520. .script-list li.blacklisted{
  521. display: var(--blacklisted-display);
  522. }
  523. .script-list li.hidden{
  524. display: var(--hidden-display);
  525. }
  526. .install-link.install-status-checking,
  527. .install-link.install-status-checking:visited,
  528. .install-link.install-status-checking:active,
  529. .install-link.install-status-checking:hover,
  530. .install-help-link.install-status-checking {
  531. background-color: #405458;
  532. }
  533. `
  534.  
  535. const window = {};
  536.  
  537. /** @param {typeof WinComm.createInstance} createInstance */
  538. function contentScriptText(shObject, createInstance) {
  539.  
  540. /*
  541. *
  542. return new Promise((resolve, reject) => {
  543. const external = unsafeWindow.external;
  544. console.log(334, external)
  545. const scriptHandler = GM.info.scriptHandler;
  546. if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey' ) {
  547. external.Violentmonkey.isInstalled(name, namespace).then((data) => resolve(data));
  548. return;
  549. }
  550. if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') {
  551. external.Tampermonkey.isInstalled(name, namespace, (data) => {
  552. (data.installed) ? resolve(data.version) : resolve();
  553. });
  554. return;
  555. }
  556. resolve();
  557. });
  558. */
  559.  
  560.  
  561. const { scriptHandler, scriptName, scriptVersion, scriptNamespace, communicationId } = shObject;
  562.  
  563. const wincomm = createInstance(communicationId);
  564.  
  565. const external = window.external;
  566.  
  567. if (external[scriptHandler]) 1;
  568. else if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey') scriptHandler = 'Violentmonkey';
  569. else if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') scriptHandler = 'Tampermonkey';
  570.  
  571. const manager = external[scriptHandler];
  572.  
  573. if (!manager) {
  574.  
  575. wincomm.send('userScriptManagerNotDetected', {
  576. code: 1
  577. });
  578. return;
  579.  
  580. }
  581.  
  582.  
  583. const pnIsInstalled2 = (type, scriptName, scriptNamespace) => new Promise((resolve, reject) => {
  584. const result = manager.isInstalled(scriptName, scriptNamespace);
  585. if (result instanceof Promise) {
  586. result.then((result) => resolve({
  587. type,
  588. result: typeof result === 'string' ? { version: result } : result
  589. })).catch(reject);
  590. } else {
  591. resolve({
  592. type,
  593. result: typeof result === 'string' ? { version: result } : result
  594. })
  595. }
  596.  
  597. }).catch(console.warn);
  598.  
  599.  
  600. const pnIsInstalled3 = (type, scriptName, scriptNamespace) => new Promise((resolve, reject) => {
  601. try {
  602. manager.isInstalled(scriptName, scriptNamespace, (result) => {
  603. resolve({
  604. type,
  605. result: typeof result === 'string' ? { version: result } : result
  606. })
  607.  
  608. });
  609. } catch (e) {
  610. reject(e);
  611. }
  612. }).catch(console.warn);
  613.  
  614.  
  615.  
  616. const enableScriptInstallChecker = (r) => {
  617.  
  618. const { type, result } = r;
  619. let version = result.version;
  620. // console.log(type, result, version)
  621. if (version !== scriptVersion) return;
  622.  
  623. const pnIsInstalled = type < 25 ? pnIsInstalled2 : pnIsInstalled3;
  624.  
  625. wincomm.hook('_$GreasyFork$Msg$OnScriptInstallCheck', {
  626.  
  627. 'installedVersion.req': (d, evt) => {
  628. pnIsInstalled(type, d.data.name, d.data.namespace).then((r) => {
  629. if (r && 'result' in r) {
  630. wincomm.response(evt, 'installedVersion.res', {
  631. version: r.result ? (r.result.version || '') : ''
  632. });
  633. }
  634. })
  635. }
  636.  
  637. });
  638.  
  639. wincomm.send('ready', { type });
  640.  
  641. // console.log('enableScriptInstallChecker', r)
  642.  
  643.  
  644. }
  645.  
  646. const kl = manager.isInstalled.length;
  647.  
  648. if (!(kl === 2 || kl === 3)) return;
  649. const puds = kl === 2 ? [
  650. pnIsInstalled2(21, scriptName, scriptNamespace),
  651. pnIsInstalled2(20, scriptName, '')
  652. ] : [
  653. pnIsInstalled3(31, scriptName, scriptNamespace),
  654. pnIsInstalled3(30, scriptName, '')
  655. ];
  656.  
  657. Promise.all(puds).then((rs) => {
  658. const [r1, r0] = rs;
  659. if (r0 && r0.result && r0.result.version) enableScriptInstallChecker(r0); // '3.1.4'
  660. else if (r1 && r1.result && r1.result.version) enableScriptInstallChecker(r1);
  661. });
  662.  
  663.  
  664.  
  665. // console.log(327, shObject, scriptHandler);
  666.  
  667. }
  668.  
  669.  
  670.  
  671. return { fields, logo, locales, blacklist, settingsCSS, pageCSS, contentScriptText }
  672.  
  673.  
  674.  
  675. })();
  676.  
  677. (async () => {
  678.  
  679. function fixValue(key, def, test) {
  680. return GM.getValue(key, def).then((v) => test(v) || GM.deleteValue(key))
  681. }
  682.  
  683. function numberArr(arrVal) {
  684. if (!arrVal || typeof arrVal.length !== 'number') return [];
  685. return arrVal.filter(e => typeof e === 'number' && !isNaN(e))
  686. }
  687.  
  688. const isScriptFirstUse = await GM.getValue('firstUse', true);
  689. await Promise.all([
  690. fixValue('hiddenList', [], v => v && typeof v === 'object' && typeof v.length === 'number' && (v.length === 0 || typeof v[0] === 'number')),
  691. fixValue('lastMilestone', 0, v => v && typeof v === 'number' && v >= 0)
  692. ])
  693.  
  694. function createRE(t, ...opt) {
  695. try {
  696. return new RegExp(t, ...opt);
  697. } catch (e) { }
  698. return null;
  699. }
  700.  
  701. const id = 'greasyfork-plus';
  702. const title = `${GM.info.script.name} v${GM.info.script.version} Settings`;
  703. const fields = mWindow.fields;
  704. const logo = mWindow.logo;
  705. const nonLatins = /[^\p{Script=Latin}\p{Script=Common}\p{Script=Inherited}]/gu;
  706. const blacklist = createRE((mWindow.blacklist || []).join('|'), 'giu');
  707. const hiddenList = numberArr(await GM.getValue('hiddenList', []));
  708. const lang = document.documentElement.lang;
  709. const locales = mWindow.locales;
  710.  
  711. function hiddenListStrToArr(str) {
  712. if (!str || typeof str !== 'string') str = '';
  713. return [...new Set(str ? numberArr(str.split(',').map(e => parseInt(e))) : [])];
  714. }
  715.  
  716. const gmc = new GM_config({
  717. id,
  718. title,
  719. fields,
  720. css: mWindow.settingsCSS,
  721. events: {
  722. init: () => {
  723. gmc.initializedResolve && gmc.initializedResolve();
  724. gmc.initializedResolve = null;
  725.  
  726. },
  727. /** @param {Document} document */
  728. open: async (document) => {
  729. const textarea = document.querySelector(`#${id}_field_hiddenList`);
  730.  
  731. const hiddenSet = new Set(numberArr(await GM.getValue('hiddenList', [])));
  732. if (hiddenSet.size !== 0) {
  733. const unsavedHiddenList = hiddenListStrToArr(gmc.get('hiddenList'));
  734. const unsavedHiddenSet = new Set(unsavedHiddenList);
  735.  
  736. const hasDifferentItems = [...hiddenSet].some(item => !unsavedHiddenSet.has(item)) || [...unsavedHiddenSet].some(item => !hiddenSet.has(item));
  737.  
  738. if (hasDifferentItems) {
  739.  
  740. gmc.fields.hiddenList.value = [...hiddenSet].sort((a, b) => a - b).join(', ');
  741.  
  742. gmc.close();
  743. gmc.open();
  744.  
  745. }
  746.  
  747.  
  748. }
  749.  
  750. const resize = (target) => {
  751. target.style.height = '';
  752. target.style.height = `${target.scrollHeight}px`;
  753. };
  754.  
  755. if (textarea) {
  756. resize(textarea);
  757. textarea.addEventListener('input', (event) => resize(event.target));
  758.  
  759. }
  760.  
  761. document.body.addEventListener('mousedown', (event) => {
  762. if (event.detail > 1 && !event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey && !event.defaultPrevented) {
  763. event.preventDefault();
  764. event.stopPropagation();
  765. event.stopImmediatePropagation();
  766. }
  767. }, true);
  768. },
  769. save: async (forgotten) => {
  770.  
  771. if (gmc.isOpen) {
  772. await GM.setValue('hiddenList', hiddenListStrToArr(forgotten.hiddenList));
  773.  
  774. UU.alert('settings saved');
  775. gmc.close();
  776. setTimeout(() => window.location.reload(false), 500);
  777. }
  778. }
  779. }
  780. });
  781. gmc.initialized = new Promise(r => (gmc.initializedResolve = r));
  782. await gmc.initialized.then();
  783. const customBlacklistRE = createRE((gmc.get('customBlacklist') || '').replace(/\s/g, '').split(',').join('|'), 'giu');
  784.  
  785. if (typeof GM.registerMenuCommand === 'function') {
  786. GM.registerMenuCommand('Configure', () => gmc.open());
  787. GM.registerMenuCommand('Reset Everything', () => {
  788. Promise.all([
  789. GM.deleteValue('hiddenList'),
  790. GM.deleteValue('lastMilestone'),
  791. GM.deleteValue('firstUse')
  792. ]).then(() => {
  793. setTimeout(() => window.location.reload(false), 50);
  794. })
  795. });
  796. }
  797.  
  798. UU.init({ id, logging: gmc.get('logging') });
  799. UU.log(nonLatins);
  800. UU.log(blacklist);
  801. UU.log(hiddenList);
  802.  
  803. const _VM = (typeof VM !== 'undefined' ? VM : null) || {
  804. shortcut: {
  805. register: () => { }
  806. }
  807. };
  808. let avoidDuplication = 0;
  809. const avoidDuplicationF = () => {
  810. const p = avoidDuplication;
  811. avoidDuplication = Date.now();
  812. if (avoidDuplication - p < 30) return false;
  813. return true;
  814. }
  815. const shortcuts = [
  816. ['ctrlcmd-alt-s', () => avoidDuplicationF() && gmc.open()],
  817. ['ctrlcmd-alt-ß', () => avoidDuplicationF() && gmc.open()],
  818. ['ctrlcmd-alt-b', () => avoidDuplicationF() && toggleListDisplayingItem('blacklisted')],
  819. ['ctrlcmd-alt-∫', () => avoidDuplicationF() && toggleListDisplayingItem('blacklisted')],
  820. ['ctrlcmd-alt-h', () => avoidDuplicationF() && toggleListDisplayingItem('hidden')],
  821. ['ctrlcmd-alt-˙', () => avoidDuplicationF() && toggleListDisplayingItem('hidden')]
  822. ]
  823. for (const [scKey, scFn] of shortcuts) {
  824. _VM.shortcut.register(scKey, scFn);
  825. }
  826.  
  827. const addSettingsToMenu = () => {
  828. const nav = document.querySelector('#site-nav > nav')
  829. if (!nav) return;
  830.  
  831. const scriptName = GM.info.script.name;
  832. const scriptVersion = GM.info.script.version;
  833. const menu = document.createElement('li');
  834. menu.classList.add(id);
  835. menu.setAttribute('alt', `${scriptName} ${scriptVersion}`);
  836. menu.setAttribute('title', `${scriptName} ${scriptVersion}`);
  837. const link = document.createElement('a');
  838. link.setAttribute('href', '#');
  839. link.textContent = GM.info.script.name;
  840. menu.appendChild(link);
  841. nav.insertBefore(menu, document.querySelector('#site-nav > nav > li:first-of-type'));
  842.  
  843. menu.addEventListener('click', (e) => {
  844. e.preventDefault();
  845. e.stopPropagation();
  846. e.stopImmediatePropagation();
  847. gmc.open();
  848. });
  849. };
  850.  
  851.  
  852. const toggleListDisplayingItem = (t) => {
  853.  
  854. const m = document.documentElement;
  855.  
  856. const p = t + '-shown';
  857. let currentIsShown = m.hasAttribute(p)
  858. if (!currentIsShown) {
  859. m.setAttribute(p, '')
  860. } else {
  861. m.removeAttribute(p)
  862. }
  863.  
  864. }
  865.  
  866. const createListOptionGroup = () => {
  867.  
  868. const html = `<div class="list-option-group" id="${id}-options">${GM.info.script.name} Lists:<ul>
  869. <li class="list-option blacklisted"><a href="#" id="hyperlink-35389"></a></li>
  870. <li class="list-option hidden"><a href="#" id="hyperlink-40361"></a></li>
  871. </ul></div>`;
  872. const firstOptionGroup = document.querySelector('.list-option-groups > div');
  873. firstOptionGroup && firstOptionGroup.insertAdjacentHTML('beforebegin', html);
  874.  
  875. const blacklistedOption = document.querySelector(`#${id}-options li.blacklisted`);
  876. blacklistedOption && blacklistedOption.addEventListener('click', (evt) => {
  877. evt.preventDefault();
  878. toggleListDisplayingItem('blacklisted');
  879. }, false);
  880.  
  881. const hiddenOption = document.querySelector(`#${id}-options li.hidden`);
  882. hiddenOption && hiddenOption.addEventListener('click', (evt) => {
  883. evt.preventDefault();
  884. toggleListDisplayingItem('hidden');
  885. }, false);
  886.  
  887. }
  888.  
  889. const addOptions = () => {
  890.  
  891. const gn = () => {
  892.  
  893. let aBlackList = document.querySelector('#hyperlink-35389');
  894. let aHidden = document.querySelector('#hyperlink-40361');
  895. if (!aBlackList || !aHidden) return;
  896. aBlackList.textContent = `Blacklisted scripts (${document.querySelectorAll('.script-list li.blacklisted').length})`;
  897. aHidden.textContent = `Hidden scripts (${document.querySelectorAll('.script-list li.hidden').length})`
  898.  
  899. }
  900. const callback = (entries) => {
  901. if (entries && entries.length >= 1) requestAnimationFrame(gn);
  902. }
  903.  
  904. const setupScriptList = async () => {
  905. let scriptList;
  906. let i = 8;
  907. while (i-- > 0) {
  908. scriptList = document.querySelector('.script-list li')
  909. if (scriptList) scriptList = scriptList.closest('.script-list')
  910. if (scriptList) break;
  911. await new Promise(r => requestAnimationFrame(r))
  912. }
  913. if (!scriptList) return;
  914. createListOptionGroup();
  915. const mo = new MutationObserver(callback);
  916. mo.observe(scriptList, { childList: true, subtree: true });
  917. gn();
  918. }
  919. setupScriptList();
  920.  
  921. };
  922.  
  923.  
  924. /**
  925. * Get script data from Greasy Fork API
  926. *
  927. * @param {number} id Script ID
  928. * @returns {Promise} Script data
  929. */
  930. let networkMP1 = Promise.resolve();
  931. let networkMP2 = Promise.resolve();
  932. let previousIsCache = false;
  933. // let ss = [];
  934. // var sum = function(nums) {
  935. // var total = 0;
  936. // for (var i = 0, len = nums.length; i < len; i++) total += nums[i];
  937. // return total;
  938. // };
  939. const getScriptData = async (id, noCache) => {
  940. if (!(id >= 0)) return Promise.resolve()
  941. const url = `https://${window.location.hostname}/scripts/${id}.json`;
  942. return new Promise((resolve, reject) => {
  943.  
  944. networkMP1 = networkMP1.then(() => new Promise(unlock => {
  945.  
  946. const maxAgeInSeconds = 900;
  947. const rd = previousIsCache ? 1 : Math.floor(Math.random() * 80 + 80);
  948. let fetchStart = 0;
  949. new Promise(r => setTimeout(r, rd))
  950. .then(() => {
  951. fetchStart = Date.now();
  952. })
  953. .then(() => fetch(url, noCache ? {
  954. method: 'GET',
  955. cache: 'reload',
  956. credentials: 'omit',
  957. headers: new Headers({
  958. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  959. })
  960. } : {
  961. method: 'GET',
  962. cache: 'force-cache',
  963. credentials: 'omit',
  964. headers: new Headers({
  965. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  966. }),
  967. }))
  968. .then((response) => {
  969.  
  970. let fetchStop = Date.now();
  971. // const dd = fetchStop - fetchStart;
  972. // dd (cache) = {min: 1, max: 8, avg: 3.7}
  973. // dd (normal) = {min: 136, max: 316, avg: 162.62}
  974.  
  975. // ss.push(dd)
  976. // ss.maxValue = Math.max(...ss);
  977. // ss.minValue = Math.min(...ss);
  978. // ss.avgValue = sum(ss)/ss.length;
  979. // console.log(dd)
  980. // console.log(ss)
  981. previousIsCache = (fetchStop - fetchStart) < (3.7 + 162.62) / 2;
  982. UU.log(`${response.status}: ${response.url}`)
  983. // UU.log(response)
  984. if (response.ok === true) {
  985. unlock();
  986. return response.json()
  987. }
  988. if (response.status === 503) {
  989. return new Promise(r => setTimeout(r, 270 + rd)).then(() => {
  990. unlock();
  991. return getScriptData(id, true);
  992. });
  993. }
  994. console.warn(response);
  995. new Promise(r => setTimeout(r, 470)).then(unlock); // reload later
  996. })
  997. .then((data) => resolve(data))
  998. .catch((e) => {
  999. unlock();
  1000. UU.log(id, url)
  1001. console.warn(e)
  1002. // reject(e)
  1003. })
  1004.  
  1005. })).catch(() => { })
  1006.  
  1007. });
  1008. }
  1009.  
  1010. /**
  1011. * Get user data from Greasy Fork API
  1012. *
  1013. * @param {string} userID User ID
  1014. * @returns {Promise} User data
  1015. */
  1016. const getUserData = (userID, noCache) => {
  1017.  
  1018. if (!(userID >= 0)) return Promise.resolve()
  1019.  
  1020. const url = `https://${window.location.hostname}/users/${userID}.json`;
  1021. return new Promise((resolve, reject) => {
  1022.  
  1023.  
  1024. networkMP2 = networkMP2.then(() => new Promise(unlock => {
  1025.  
  1026. const maxAgeInSeconds = 900;
  1027. const rd = Math.floor(Math.random() * 80 + 80);
  1028.  
  1029. new Promise(r => setTimeout(r, rd))
  1030. .then(() => fetch(url, noCache ? {
  1031. method: 'GET',
  1032. cache: 'reload',
  1033. credentials: 'omit',
  1034. headers: new Headers({
  1035. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  1036. })
  1037. } : {
  1038. method: 'GET',
  1039. cache: 'force-cache',
  1040. credentials: 'omit',
  1041. headers: new Headers({
  1042. 'Cache-Control': `max-age=${maxAgeInSeconds}`,
  1043. }),
  1044. }))
  1045. .then((response) => {
  1046. UU.log(`${response.status}: ${response.url}`)
  1047. if (response.ok === true) {
  1048. unlock();
  1049. return response.json()
  1050. }
  1051. if (response.status === 503) {
  1052. return new Promise(r => setTimeout(r, 270 + rd)).then(() => {
  1053. unlock();
  1054. return getUserData(userID, true); // reload later
  1055. });
  1056. }
  1057. console.warn(response);
  1058. new Promise(r => setTimeout(r, 470)).then(unlock);
  1059. })
  1060. .then((data) => resolve(data))
  1061. .catch((e) => {
  1062. setTimeout(() => {
  1063. unlock()
  1064. }, 270)
  1065. UU.log(userID, url)
  1066. console.warn(e)
  1067. // reject(e)
  1068. })
  1069.  
  1070.  
  1071.  
  1072. })).catch(() => { })
  1073.  
  1074. });
  1075. }
  1076. const getTotalInstalls = (data) => {
  1077. if (!data || !data.scripts) return;
  1078. return new Promise((resolve, reject) => {
  1079. const totalInstalls = [];
  1080.  
  1081. data.scripts.forEach((element) => {
  1082. totalInstalls.push(parseInt(element.total_installs, 10));
  1083. });
  1084.  
  1085. resolve(totalInstalls.reduce((a, b) => a + b, 0));
  1086. });
  1087. };
  1088.  
  1089.  
  1090. const communicationId = WinComm.newCommunicationId();
  1091. const wincomm = WinComm.createInstance(communicationId);
  1092.  
  1093.  
  1094. const isInstalled = (script) => {
  1095. return new Promise((resolve, reject) => {
  1096.  
  1097. promiseScriptCheck.then(d => {
  1098.  
  1099. if (!d) return null;
  1100.  
  1101. const data = d.data;
  1102. const al = data.type % 10;
  1103. if (al === 0) {
  1104. // no namespace
  1105. resolve([null, script.name, '']);
  1106. } else if (al === 1) {
  1107. // namespace
  1108.  
  1109. if (!script.namespace) {
  1110.  
  1111. getScriptData(script.id).then((script) => {
  1112. resolve([null, script.name, script.namespace]);
  1113. });
  1114.  
  1115. } else {
  1116.  
  1117. resolve([null, script.name, script.namespace]);
  1118. }
  1119.  
  1120. }
  1121.  
  1122.  
  1123. })
  1124.  
  1125.  
  1126. }).then((res) => {
  1127.  
  1128.  
  1129. return new Promise((resolve, reject) => {
  1130.  
  1131. if (!res) return '';
  1132.  
  1133.  
  1134. const [_, name, namespace] = res;
  1135. wincomm.request('installedVersion.req', {
  1136. name,
  1137. namespace
  1138. }).then(d => {
  1139. resolve(d.data.version)
  1140. })
  1141.  
  1142. })
  1143.  
  1144. })
  1145.  
  1146. /*
  1147. const external = unsafeWindow.external;
  1148. const scriptHandler = GM.info.scriptHandler;
  1149. if (external && external.Violentmonkey && (scriptHandler || 'Violentmonkey') === 'Violentmonkey') {
  1150. external.Violentmonkey.isInstalled(name, namespace).then((data) => resolve(data));
  1151. return;
  1152. }
  1153. if (external && external.Tampermonkey && (scriptHandler || 'Tampermonkey') === 'Tampermonkey') {
  1154. external.Tampermonkey.isInstalled(name, namespace, (data) => {
  1155. (data.installed) ? resolve(data.version) : resolve();
  1156. });
  1157. return;
  1158. }
  1159. */
  1160.  
  1161.  
  1162. };
  1163.  
  1164. const compareVersions = (v1, v2) => {
  1165. if (!v1 || !v2) return NaN;
  1166. if (v1 === null || v2 === null) return NaN;
  1167. if (v1 === v2) return 0;
  1168.  
  1169. const sv1 = v1.split('.').map((index) => parseInt(index));
  1170. const sv2 = v2.split('.').map((index) => parseInt(index));
  1171.  
  1172. for (let index = 0; index < Math.max(sv1.length, sv2.length); index++) {
  1173. if (isNaN(sv1[index]) || isNaN(sv2[index])) return NaN;
  1174. if (sv1[index] > sv2[index]) return 1;
  1175. if (sv1[index] < sv2[index]) return -1;
  1176. }
  1177.  
  1178. return 0;
  1179. };
  1180.  
  1181.  
  1182. /**
  1183. * Return label for the hide script button
  1184. *
  1185. * @param {boolean} hidden Is hidden
  1186. * @returns {string} Label
  1187. */
  1188. const blockLabel = (hidden) => {
  1189. return hidden ? (locales[lang] ? locales[lang].notHide : locales.en.notHide) : (locales[lang] ? locales[lang].hide : locales.en.hide)
  1190. }
  1191.  
  1192. /**
  1193. * Return label for the install button
  1194. *
  1195. * @param {number} update Update value
  1196. * @returns {string} Label
  1197. */
  1198. const installLabel = (update) => {
  1199. switch (update) {
  1200. case 0: {
  1201. return locales[lang] ? locales[lang].reinstall : locales.en.reinstall
  1202. }
  1203. case 1: {
  1204. return locales[lang] ? locales[lang].update : locales.en.update
  1205. }
  1206. case -1: {
  1207. return locales[lang] ? locales[lang].downgrade : locales.en.downgrade
  1208. }
  1209. default: {
  1210. return locales[lang] ? locales[lang].install : locales.en.install
  1211. }
  1212. }
  1213. }
  1214.  
  1215. const hideBlacklistedScript = (element, list) => {
  1216. if (!element) return;
  1217. const scriptLink = element.querySelector('.script-link')
  1218.  
  1219. const name = scriptLink ? scriptLink.textContent : '';
  1220. const descriptionElem = element.querySelector('.script-description')
  1221. const description = descriptionElem ? descriptionElem.textContent : '';
  1222.  
  1223. if (!name) return;
  1224.  
  1225. switch (list) {
  1226. case 'nonLatins':
  1227. if ((nonLatins.test(name) || nonLatins.test(description)) && !element.classList.contains('blacklisted')) {
  1228. element.classList.add('blacklisted', 'non-latins');
  1229. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  1230. let scriptLink = element.querySelector('.script-link');
  1231. if (scriptLink) { scriptLink.textContent += ' (non-latin)'; }
  1232. }
  1233. }
  1234. break;
  1235. case 'blacklist':
  1236. if (blacklist && (blacklist.test(name) || blacklist.test(description)) && !element.classList.contains('blacklisted')) {
  1237. element.classList.add('blacklisted', 'blacklist');
  1238. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  1239. let scriptLink = element.querySelector('.script-link');
  1240. if (scriptLink) { scriptLink.textContent += ' (blacklist)'; }
  1241. }
  1242. }
  1243. break;
  1244. case 'customBlacklist': {
  1245. const customBlacklist = customBlacklistRE;
  1246. if (customBlacklist && (customBlacklist.test(name) || customBlacklist.test(description)) && !element.classList.contains('blacklisted')) {
  1247. element.classList.add('blacklisted', 'custom-blacklist');
  1248. if (gmc.get('hideBlacklistedScripts') && gmc.get('debugging')) {
  1249. let scriptLink = element.querySelector('.script-link');
  1250. if (scriptLink) { scriptLink.textContent += ' (custom-blacklist)'; }
  1251. }
  1252. }
  1253. break;
  1254. }
  1255. default:
  1256. UU.log('No blacklists');
  1257. break;
  1258. }
  1259. };
  1260.  
  1261. const hideHiddenScript = (element, id, list) => {
  1262. id = +id;
  1263. if (!(id >= 0)) return;
  1264.  
  1265. const isInHiddenList = () => hiddenList.indexOf(id) !== -1;
  1266. const updateScriptLink = (shouldHide) => {
  1267. if (gmc.get('hideHiddenScript') && gmc.get('debugging')) {
  1268. let scriptLink = element.querySelector('.script-link');
  1269. if (scriptLink) {
  1270. if (shouldHide) {
  1271. scriptLink.innerHTML += ' (hidden)';
  1272. } else {
  1273. scriptLink.innerHTML = scriptLink.innerHTML.replace(' (hidden)', '');
  1274. }
  1275. }
  1276. }
  1277. };
  1278.  
  1279. // Check for initial state and set it
  1280. if (isInHiddenList()) {
  1281. element.classList.add('hidden');
  1282. updateScriptLink(true);
  1283. }
  1284.  
  1285. // Add button to hide the script
  1286. const insertButtonHTML = (selector, html) => {
  1287. const target = element.querySelector(selector);
  1288. if (!target) return;
  1289. let p = document.createElement('template');
  1290. p.innerHTML = html;
  1291. target.parentNode.insertBefore(p.content.firstChild, target.nextSibling);
  1292. };
  1293.  
  1294. const isHidden = element.classList.contains('hidden');
  1295. const blockButtonHTML = `<span class=block-button role=button style-16377>${blockLabel(isHidden)}</span>`;
  1296. const blockButtonHeaderHTML = `<span class=block-button role=button style-77329 style="">${blockLabel(isHidden)}</span>`;
  1297.  
  1298. insertButtonHTML('.badge-js, .badge-css', blockButtonHTML);
  1299. insertButtonHTML('header h2', blockButtonHeaderHTML);
  1300.  
  1301. // Add event listener
  1302. const button = element.querySelector('.block-button');
  1303. if (button) {
  1304. button.addEventListener('click', (event) => {
  1305. event.stopPropagation();
  1306. event.stopImmediatePropagation();
  1307.  
  1308. if (!isInHiddenList()) {
  1309. hiddenList.push(id);
  1310. GM.setValue('hiddenList', hiddenList);
  1311.  
  1312. element.classList.add('hidden');
  1313. updateScriptLink(true);
  1314.  
  1315. } else {
  1316. const index = hiddenList.indexOf(id);
  1317. hiddenList.splice(index, 1);
  1318. GM.setValue('hiddenList', hiddenList);
  1319.  
  1320. element.classList.remove('hidden');
  1321. updateScriptLink(false);
  1322. }
  1323.  
  1324. const blockBtn = element.querySelector('.block-button');
  1325. if (blockBtn) blockBtn.textContent = blockLabel(element.classList.contains('hidden'));
  1326. });
  1327. }
  1328. };
  1329.  
  1330. const insertButtonHTML = (element, selector, html) => {
  1331. const target = element.querySelector(selector);
  1332. if (!target) return;
  1333. let p = document.createElement('template');
  1334. p.innerHTML = html;
  1335. let button = p.content.firstChild
  1336. target.parentNode.insertBefore(button, target.nextSibling);
  1337. return button;
  1338. };
  1339.  
  1340. const addInstallButton = (element, url) => {
  1341. return insertButtonHTML(element, '.badge-js, .badge-css', `<a class="install-link" href="${url}" style-54998></a>`);
  1342. };
  1343.  
  1344. const showInstallButton = async (scriptID, element) => {
  1345.  
  1346.  
  1347. // if(document.querySelector(`li[data-script-id="${scriptID}"]`))
  1348. let _script = null;
  1349. if (element.nodeName === 'LI' && element.hasAttribute('data-script-id') && element.getAttribute('data-script-id') === `${scriptID}` && element.getAttribute('data-script-language') === 'js') {
  1350.  
  1351. const version = element.getAttribute('data-script-version') || ''
  1352. const name = element.getAttribute('data-script-name') || ''
  1353. let scriptFilename = version ? `${scriptID}-${version}` : '';
  1354. _script = {
  1355. id: +scriptID,
  1356. name: name,
  1357. code_url: `https://greasyfork.org/scripts/${scriptID}/code/${scriptFilename}.user.js`,
  1358. version: version
  1359. }
  1360.  
  1361. }
  1362.  
  1363. const script = _script || (await getScriptData(scriptID));
  1364. if (!script) return;
  1365. const button = addInstallButton(element, script.code_url);
  1366. button.classList.add('install-status-checking');
  1367. button.textContent = `${installLabel()} ${script.version}`;
  1368.  
  1369. const installed = await isInstalled(script)
  1370.  
  1371.  
  1372. const update = compareVersions(script.version, installed); // NaN 1 -1 0
  1373.  
  1374. const label = installLabel(update);
  1375.  
  1376. button.textContent = `${label} ${script.version}`;
  1377. button.classList.remove('install-status-checking');
  1378.  
  1379. }
  1380.  
  1381.  
  1382. const foundScriptList = async (scriptList) => {
  1383.  
  1384. let rid = 0;
  1385. let g = () => {
  1386. if (!scriptList || scriptList.isConnected !== true) return;
  1387.  
  1388. const scriptElements = scriptList.querySelectorAll('li[data-script-id]:not([e8kk])');
  1389.  
  1390. for (const element of scriptElements) {
  1391. element.setAttribute('e8kk', '1');
  1392.  
  1393. const scriptID = +element.getAttribute('data-script-id');
  1394. if (!(scriptID > 0)) continue;
  1395.  
  1396. // blacklisted scripts
  1397. if (gmc.get('nonLatins')) hideBlacklistedScript(element, 'nonLatins');
  1398. if (gmc.get('blacklist')) hideBlacklistedScript(element, 'blacklist');
  1399. if (gmc.get('customBlacklist')) hideBlacklistedScript(element, 'customBlacklist');
  1400.  
  1401. // hidden scripts
  1402. if (gmc.get('hideHiddenScript')) hideHiddenScript(element, scriptID, true);
  1403.  
  1404. // install button
  1405. if (gmc.get('showInstallButton')) {
  1406. showInstallButton(scriptID, element)
  1407. }
  1408. }
  1409.  
  1410. }
  1411. let f = (entries) => {
  1412. const tid = ++rid
  1413. if (entries && entries.length) requestAnimationFrame(() => {
  1414. if (tid === rid) g();
  1415. });
  1416. }
  1417. let mo = new MutationObserver(f);
  1418. mo.observe(scriptList, { subtree: true, childList: true });
  1419.  
  1420. g();
  1421.  
  1422. }
  1423.  
  1424. let promiseScriptCheckResolve = null;
  1425. const promiseScriptCheck = new Promise(resolve => {
  1426. promiseScriptCheckResolve = resolve
  1427. });
  1428.  
  1429. const onReady = async () => {
  1430.  
  1431. const gminfo = GM.info || 0;
  1432. if (gminfo) {
  1433.  
  1434. const gminfoscript = gminfo.script || 0;
  1435.  
  1436.  
  1437. const scriptHandlerObject = {
  1438. scriptHandler: gminfo.scriptHandler || '',
  1439. scriptName: gminfoscript.name || '',
  1440. scriptVersion: gminfoscript.version || '',
  1441. scriptNamespace: gminfoscript.namespace || '',
  1442. communicationId
  1443. };
  1444.  
  1445.  
  1446. wincomm.hook('_$GreasyFork$Msg$OnScriptInstallFeedback',
  1447. {
  1448.  
  1449. ready: (d, evt) => promiseScriptCheckResolve(d),
  1450. userScriptManagerNotDetected: (d, evt) => promiseScriptCheckResolve(null),
  1451. 'installedVersion.res': wincomm.handleResponse
  1452.  
  1453.  
  1454. })
  1455.  
  1456.  
  1457. document.head.appendChild(document.createElement('script')).textContent = `;(${mWindow.contentScriptText})(${JSON.stringify(scriptHandlerObject)}, ${WinComm.createInstance});`;
  1458.  
  1459.  
  1460. }
  1461.  
  1462.  
  1463. addSettingsToMenu();
  1464.  
  1465.  
  1466. setTimeout(() => {
  1467. let installBtn = document.querySelector('a[data-script-id][data-script-version]')
  1468. let scriptID = installBtn && installBtn.textContent ? +installBtn.getAttribute('data-script-id') : 0;
  1469. if (scriptID > 0) {
  1470. getScriptData(scriptID, true);
  1471. } else {
  1472.  
  1473.  
  1474. const userLink = document.querySelector('#site-nav .user-profile-link a[href]');
  1475. let userID = userLink ? userLink.getAttribute('href') : '';
  1476.  
  1477. userID = userID ? /users\/(\d+)/.exec(userID) : null;
  1478. if (userID) userID = userID[1];
  1479. if (userID) {
  1480. userID = +userID;
  1481. if (userID > 0) {
  1482. getUserData(userID, true);
  1483. }
  1484. }
  1485.  
  1486.  
  1487. }
  1488. }, 740);
  1489.  
  1490. const userLink = document.querySelector('.user-profile-link a[href]');
  1491. const userID = userLink ? userLink.getAttribute('href') : undefined;
  1492.  
  1493. // blacklisted scripts / hidden scripts / install button
  1494. if (window.location.pathname !== userID && !/discussions/.test(window.location.pathname) && (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript') || gmc.get('showInstallButton'))) {
  1495.  
  1496. const scriptList = document.querySelector('.script-list');
  1497. if (scriptList) {
  1498. foundScriptList(scriptList);
  1499. } else {
  1500. const timeout = Date.now() + 3000;
  1501. /** @type {MutationObserver | null} */
  1502. let mo = null;
  1503. const mutationCallbackForScriptList = () => {
  1504. if (!mo) return;
  1505. const scriptList = document.querySelector('.script-list');
  1506. if (scriptList) {
  1507. mo.disconnect();
  1508. mo.takeRecords();
  1509. mo = null;
  1510. foundScriptList(scriptList);
  1511. } else if (Date.now() > timeout) {
  1512. mo.disconnect();
  1513. mo.takeRecords();
  1514. mo = null;
  1515. }
  1516. }
  1517. mo = new MutationObserver(mutationCallbackForScriptList);
  1518. mo.observe(document, { subtree: true, childList: true });
  1519. }
  1520.  
  1521.  
  1522. // hidden scripts on details page
  1523. const installLinkElement = document.querySelector('#script-info .install-link[data-script-id]');
  1524. if (gmc.get('hideHiddenScript') && installLinkElement) {
  1525. const id = +installLinkElement.getAttribute('data-script-id');
  1526. hideHiddenScript(document.querySelector('#script-info'), id, false);
  1527. }
  1528.  
  1529. // add options and style for blacklisted/hidden scripts
  1530. if (gmc.get('hideBlacklistedScripts') || gmc.get('hideHiddenScript')) {
  1531. addOptions();
  1532. UU.addStyle(mWindow.pageCSS);
  1533. }
  1534. }
  1535.  
  1536. // total installs
  1537. if (gmc.get('showTotalInstalls') && document.querySelector('#user-script-list')) {
  1538. const dailyInstalls = [];
  1539. const totalInstalls = [];
  1540.  
  1541. const dailyInstallElements = document.querySelectorAll('#user-script-list li dd.script-list-daily-installs');
  1542. for (const element of dailyInstallElements) {
  1543. dailyInstalls.push(parseInt(element.textContent.replace(/\D/g, ''), 10));
  1544. }
  1545.  
  1546. const totalInstallElements = document.querySelectorAll('#user-script-list li dd.script-list-total-installs');
  1547. for (const element of totalInstallElements) {
  1548. totalInstalls.push(parseInt(element.textContent.replace(/\D/g, ''), 10));
  1549. }
  1550.  
  1551. const dailyInstallsSum = dailyInstalls.reduce((a, b) => a + b, 0);
  1552. const totalInstallsSum = totalInstalls.reduce((a, b) => a + b, 0);
  1553.  
  1554. const convertLi = (li) => {
  1555.  
  1556. if (!li) return null;
  1557. const a = li.firstElementChild
  1558. if (a === null) return li;
  1559. if (a === li.lastElementChild && a.nodeName === 'A') return a;
  1560.  
  1561.  
  1562. return null;
  1563. }
  1564.  
  1565. const dailyOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(1)'));
  1566. dailyOption && dailyOption.insertAdjacentHTML('beforeend', `<span> (${dailyInstallsSum.toLocaleString()})</span>`);
  1567.  
  1568. const totalOption = convertLi(document.querySelector('#script-list-sort .list-option:nth-child(2)'));
  1569. totalOption && totalOption.insertAdjacentHTML('beforeend', `<span> (${totalInstallsSum.toLocaleString()})</span>`);
  1570. }
  1571.  
  1572. // milestone notification
  1573. if (gmc.get('milestoneNotification')) {
  1574. const milestones = gmc.get('milestoneNotification').replace(/\s/g, '').split(',').map(Number);
  1575.  
  1576. if (!userID) return;
  1577.  
  1578. const userData = await getUserData(+userID.match(/\d+(?=\D)/g));
  1579. if (!userData) return;
  1580.  
  1581. const [totalInstalls, lastMilestone] = await Promise.all([
  1582. getTotalInstalls(userData),
  1583. GM.getValue('lastMilestone', 0)]);
  1584.  
  1585. const milestone = milestones.filter(milestone => totalInstalls >= milestone).pop();
  1586.  
  1587. UU.log(`total installs are "${totalInstalls}", milestone reached is "${milestone}", last milestone reached is "${lastMilestone}"`);
  1588.  
  1589. if (milestone <= lastMilestone) return;
  1590.  
  1591. if (milestone && milestone >= 0) {
  1592.  
  1593.  
  1594. GM.setValue('lastMilestone', milestone);
  1595.  
  1596. const lang = document.documentElement.lang;
  1597. const text = (locales[lang] ? locales[lang].milestone : locales.en.milestone).replace('$1', milestone.toLocaleString());
  1598.  
  1599. if (typeof GM.notification === 'function') {
  1600. GM.notification({
  1601. text,
  1602. title: GM.info.script.name,
  1603. image: logo,
  1604. onclick: () => {
  1605. window.location = `https://${window.location.hostname}${userID}#user-script-list-section`;
  1606. }
  1607. });
  1608. } else {
  1609. UU.alert(text);
  1610. }
  1611.  
  1612. }
  1613.  
  1614. }
  1615.  
  1616. if (isScriptFirstUse) GM.setValue('firstUse', false).then(() => {
  1617. gmc.open();
  1618. });
  1619.  
  1620. }
  1621.  
  1622.  
  1623.  
  1624. Promise.resolve().then(() => {
  1625. if (document.readyState !== 'loading') {
  1626. onReady();
  1627. } else {
  1628. window.addEventListener("DOMContentLoaded", onReady, false);
  1629. }
  1630. });
  1631.  
  1632. })();