Greasy Fork 增强

增进 Greasyfork 浏览体验。

当前为 2024-10-18 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Greasy Fork Enhance
  3. // @name:zh-CN Greasy Fork 增强
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.8.7
  6. // @description Enhance your experience at Greasyfork.
  7. // @description:zh-CN 增进 Greasyfork 浏览体验。
  8. // @match https://greasyfork.org/*
  9. // @author PRO
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_deleteValue
  13. // @grant GM_registerMenuCommand
  14. // @grant GM_unregisterMenuCommand
  15. // @grant GM_addValueChangeListener
  16. // @require https://update.greasyfork.org/scripts/470224/1459364/Tampermonkey%20Config.js
  17. // @icon https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/public/images/blacklogo16.png
  18. // @icon64 https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/public/images/blacklogo96.png
  19. // @license gpl-3.0
  20. // ==/UserScript==
  21.  
  22. (function () {
  23. 'use strict';
  24. // Judge if the script should run
  25. const no_run = [".json", ".js"];
  26. let is_run = true;
  27. const idPrefix = "greasyfork-enhance-";
  28. const name = GM_info.script.name;
  29. no_run.forEach((suffix) => {
  30. if (window.location.pathname.endsWith(suffix)) {
  31. is_run = false;
  32. }
  33. });
  34. if (!is_run) return;
  35. // Config
  36. const configDesc = {
  37. $default: {
  38. autoClose: false,
  39. },
  40. filterAndSearch: {
  41. name: "🔎 Filter and Search",
  42. type: "folder",
  43. items: {
  44. shortcut: {
  45. name: "Shortcut",
  46. title: "Enable keyboard shortcuts",
  47. type: "bool",
  48. value: true,
  49. },
  50. regexFilter: {
  51. name: "Regex filter",
  52. title: "Use regex to filter out matching scripts",
  53. value: "",
  54. },
  55. searchSyntax: {
  56. name: "*Search syntax",
  57. title: "Enable partial search syntax for Greasy Fork search bar",
  58. type: "bool",
  59. value: true,
  60. },
  61. },
  62. },
  63. codeblocks: {
  64. name: "📝 Code blocks",
  65. type: "folder",
  66. items: {
  67. autoHideCode: {
  68. name: "Auto hide code",
  69. title: "Hide long code blocks by default",
  70. type: "bool",
  71. value: true,
  72. },
  73. autoHideRows: {
  74. name: "Min rows to hide",
  75. title: "Minimum number of rows to hide",
  76. type: "int",
  77. value: 10,
  78. processor: "int_range-1-",
  79. },
  80. tabSize: {
  81. name: "Tab size",
  82. title: "Set Tab indentation size",
  83. type: "int",
  84. value: 4,
  85. processor: "int_range-0-",
  86. },
  87. animation: {
  88. name: "Animation",
  89. title: "Enable animation for toggling code blocks",
  90. type: "bool",
  91. value: true,
  92. },
  93. metadata: {
  94. name: "Metadata",
  95. title: "Parses certain script metadata and displays it on the script code page",
  96. type: "bool",
  97. value: false,
  98. },
  99. }
  100. },
  101. display: {
  102. name: "🎨 Display",
  103. type: "folder",
  104. items: {
  105. hideButtons: {
  106. name: "Hide buttons",
  107. title: "Hide floating buttons added by this script",
  108. type: "bool",
  109. value: false,
  110. },
  111. flatLayout: {
  112. name: "Flat layout",
  113. title: "Use flat layout for script list and descriptions",
  114. type: "bool",
  115. value: false,
  116. },
  117. showVersion: {
  118. name: "Show version",
  119. title: "Show version number in script list",
  120. type: "bool",
  121. value: false,
  122. },
  123. alwaysShowNotification: {
  124. name: "Always show notification",
  125. title: "Always show the notification widget",
  126. type: "bool",
  127. value: false,
  128. }
  129. },
  130. },
  131. other: {
  132. name: "🔧 Other",
  133. type: "folder",
  134. items: {
  135. shortLink: {
  136. name: "Short link",
  137. title: "Display a shortened link to current script",
  138. type: "bool",
  139. value: true,
  140. },
  141. libAlternativeUrl: {
  142. name: "Alternative URLs for library",
  143. title: "Show a list of alternative URLs for a given library",
  144. type: "bool",
  145. value: false,
  146. },
  147. imageProxy: {
  148. name: "*Image proxy",
  149. title: "Use `wsrv.nl` as proxy for user-uploaded images",
  150. type: "bool",
  151. value: false,
  152. },
  153. lazyImage: {
  154. name: "*Lazy image",
  155. title: "Load user images lazily",
  156. type: "bool",
  157. value: false,
  158. },
  159. debug: {
  160. name: "Debug",
  161. title: "Enable debug mode",
  162. type: "bool",
  163. value: false,
  164. },
  165. }
  166. }
  167. };
  168. const config = new GM_config(configDesc);
  169. const configProxy = config.proxy;
  170. // CSS
  171. const dynamicStyle = {
  172. "codeblocks.animation": `
  173. /* Toggle code animation */
  174. pre > code { transition: height 0.5s ease-in-out 0s; }
  175. /* Adapted from animate.css - https://animate.style/ */
  176. :root { --animate-duration: 1s; --animate-delay: 1s; --animate-repeat: 1; }
  177. .animate__animated { animation-duration: var(--animate-duration); animation-fill-mode: both; }
  178. .animate__animated.animate__fastest { animation-duration: calc(var(--animate-duration) / 3); }
  179. @keyframes tada {
  180. from { transform: scale3d(1, 1, 1); }
  181. 10%, 20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
  182. 30%, 50%, 70%, 90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
  183. 40%, 60%, 80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
  184. to { transform: scale3d(1, 1, 1); }
  185. }
  186. .animate__tada { animation-name: tada; }
  187. @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  188. .animate__fadeIn { animation-name: fadeIn; }
  189. @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
  190. .animate__fadeOut { -webkit-animation-name: fadeOut; animation-name: fadeOut; }
  191. `,
  192. "display.hideButtons": `div#float-buttons { display: none; }`,
  193. "display.flatLayout": `
  194. .script-list > li {
  195. &:not(.ad-entry) { padding-right: 0; }
  196. article {
  197. display: flex; flex-direction: row; justify-content: space-between; align-items: center;
  198. > .script-meta-block {
  199. width: 40%; column-gap: 0;
  200. > .inline-script-stats {
  201. margin: 0;
  202. > dd { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  203. }
  204. }
  205. > h2 {
  206. width: 60%; overflow: hidden; text-overflow: ellipsis; margin-right: 0.5em; padding-right: 0.5em; border-right: 1px solid #88888888;
  207. > .script-link { white-space: nowrap; }
  208. > .script-description { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  209. }
  210. }
  211. &[data-script-type="library"] > article {
  212. > h2 { width: 80%; }
  213. > .script-meta-block { width: 20%; column-count: 1; }
  214. }
  215. }
  216. @media (max-width: 600px) {
  217. .script-list > li {
  218. &[data-script-type="library"] > article > div.script-meta-block { width: 40%; }
  219. &:not([data-script-type="library"]) > article {
  220. display: block;
  221. > h2 { width: unset; border-right: none; }
  222. > .script-meta-block { column-count: 2; }
  223. }
  224. > article > div.script-meta-block { width: unset; column-gap: 0; }
  225. }
  226. }
  227. .showing-all-languages .badge-js, .showing-all-languages .badge-css, .script-type { display: none; }
  228. #script-info .script-meta-block { float: right; column-count: 1; max-width: 300px; border-left: 1px solid #DDDDDD; margin-left: 1em; padding-left: 1em; }
  229. #additional-info { width: calc(100% - 2em - 2px); }
  230. `,
  231. "display.showVersion": `.script-list > li[data-script-version]::before { content: "@" attr(data-script-version); position: absolute; translate: 0 -1em; color: grey; font-size: smaller; }`,
  232. };
  233. // Functions
  234. const $ = document.querySelector.bind(document);
  235. const $$ = document.querySelectorAll.bind(document);
  236. const body = $("body");
  237. function log(...args) {
  238. if (configProxy["other.debug"]) {
  239. console.log(`[${name}]`, ...args);
  240. }
  241. }
  242. function sanitify(s) {
  243. // Remove emojis (such a headache)
  244. s = s.replaceAll(/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDEFF]|\uFE0F)/g, "");
  245. // Trim spaces and newlines
  246. s = s.trim();
  247. // Replace spaces
  248. s = s.replaceAll(" ", "-");
  249. s = s.replaceAll("%20", "-");
  250. // No more multiple "-"
  251. s = s.replaceAll(/-+/g, "-");
  252. return s;
  253. }
  254. function process(outline, node) { // Add anchor and assign id to given node; Add to outline. Return true if node is actually processed.
  255. if (node.childElementCount > 1 || node.classList.length > 0) return false; // Ignore complex nodes
  256. const text = node.textContent;
  257. if (!node.id) { // If the node has no id
  258. node.id = sanitify(text); // Then assign id
  259. }
  260. // Add anchors
  261. const anchor = node.appendChild(document.createElement('a'));
  262. anchor.className = 'anchor';
  263. anchor.href = '#' + node.id;
  264. const link = outline.appendChild(document.createElement("li"))
  265. .appendChild(document.createElement("a"));
  266. link.href = "#" + node.id;
  267. link.text = text;
  268. return true;
  269. }
  270. async function animate(node, animation) {
  271. return new Promise((resolve, reject) => {
  272. node.classList.add("animate__animated", "animate__" + animation);
  273. if (node.getAnimations().length == 0) {
  274. node.classList.remove("animate__animated", "animate__" + animation);
  275. reject("No animation available");
  276. }
  277. node.addEventListener('animationend', e => {
  278. e.stopPropagation();
  279. node.classList.remove("animate__animated", "animate__" + animation);
  280. resolve("Animation ended");
  281. }, { once: true });
  282. });
  283. }
  284. async function transition(node, height) {
  285. return new Promise((resolve, reject) => {
  286. node.style.height = height;
  287. if (node.getAnimations().length == 0) {
  288. resolve("No transition available");
  289. }
  290. node.addEventListener('transitionend', e => {
  291. e.stopPropagation();
  292. resolve("Transition ended");
  293. }, { once: true });
  294. });
  295. }
  296. function copyCode() {
  297. const code = this.parentNode.nextElementSibling;
  298. const text = code.textContent;
  299. navigator.clipboard.writeText(text).then(() => {
  300. this.textContent = "Copied!";
  301. animate(this, "tada").then(() => {
  302. this.textContent = "Copy code";
  303. }, () => {
  304. window.setTimeout(() => {
  305. this.textContent = "Copy code";
  306. }, 1000);
  307. });
  308. });
  309. }
  310. function toggleCode() {
  311. const code = this.parentNode.nextElementSibling;
  312. if (code.style.height == "0px") {
  313. code.style.willChange = "height";
  314. transition(code, code.getAttribute("data-height")).then(() => {
  315. code.style.willChange = "";
  316. });
  317. animate(this, "fadeOut").then(() => {
  318. this.textContent = "Hide code";
  319. animate(this, "fadeIn");
  320. }, () => {
  321. this.textContent = "Hide code";
  322. });
  323. } else {
  324. code.style.willChange = "height";
  325. transition(code, "0px").then(() => {
  326. code.style.willChange = "";
  327. });
  328. animate(this, "fadeOut").then(() => {
  329. this.textContent = "Show code";
  330. animate(this, "fadeIn");
  331. }, () => {
  332. this.textContent = "Show code";
  333. });
  334. }
  335. }
  336. function createToolbar() {
  337. const toolbar = document.createElement("div");
  338. const copy = toolbar.appendChild(document.createElement("a"));
  339. const toggle = toolbar.appendChild(document.createElement("a"));
  340. copy.textContent = "Copy code";
  341. copy.className = "code-operation";
  342. copy.title = "Copy code to clipboard";
  343. copy.addEventListener("click", copyCode);
  344. toggle.textContent = "Hide code";
  345. toggle.classList.add("code-operation", "animate__fastest");
  346. toggle.title = "Toggle code display";
  347. toggle.addEventListener("click", toggleCode);
  348. // Css
  349. toolbar.className = "code-toolbar";
  350. return toolbar;
  351. }
  352. function injectCSS(id, css) {
  353. const style = document.head.appendChild(document.createElement("style"));
  354. style.id = idPrefix + id;
  355. style.textContent = css;
  356. }
  357. function cssHelper(id, enable) {
  358. const current = document.getElementById(idPrefix + id);
  359. if (current) {
  360. current.disabled = !enable;
  361. } else if (enable) {
  362. injectCSS(id, dynamicStyle[id]);
  363. }
  364. }
  365. // Basic css
  366. injectCSS("basic", `
  367. html { scroll-behavior: smooth; }
  368. a.anchor::before { content: "#"; }
  369. a.anchor { opacity: 0; text-decoration: none; padding: 0px 0.5em; transition: all 0.25s ease-in-out; }
  370. h1:hover>a.anchor, h2:hover>a.anchor, h3:hover>a.anchor,
  371. h4:hover>a.anchor, h5:hover>a.anchor, h6:hover>a.anchor { opacity: 1; transition: all 0.25s ease-in-out; }
  372. a.button { margin: 0.5em 0 0 0; display: flex; align-items: center; justify-content: center; text-decoration: none; color: black; background-color: #a42121ab; border-radius: 50%; width: 2em; height: 2em; font-size: 1.8em; font-weight: bold; }
  373. div.code-toolbar { display: flex; gap: 1em; }
  374. a.code-operation { cursor: pointer; font-style: italic; }
  375. div.lum-lightbox { z-index: 2; }
  376. #float-buttons { position: fixed; bottom: 1em; right: 1em; display: flex; flex-direction: column; user-select: none; z-index: 1; }
  377. aside.panel { display: none; }
  378. .dynamic-opacity { transition: opacity 0.2s ease-in-out; opacity: 0.2; }
  379. .dynamic-opacity:hover { opacity: 0.8; }
  380. input[type=file] { border-style: dashed; border-radius: 0.5em; border-color: gray; padding: 0.5em; background: rgba(169, 169, 169, 0.4); transition-property: border-color, background; transition-duration: 0.25s; transition-timing-function: ease-in-out; }
  381. input[type=file]:hover { border-color: black; background: rgba(169, 169, 169, 0.6); }
  382. input[type=file]::file-selector-button { border: 1px solid; border-radius: 0.3em; transition: background 0.25s ease-in-out; background: rgba(169, 169, 169, 0.7); }
  383. input[type=file]::file-selector-button:hover { background: rgba(169, 169, 169, 1); }
  384. table { border: 1px solid #8d8d8d; border-collapse: collapse; width: auto; }
  385. table td, table th { padding: 0.5em 0.75em; vertical-align: middle; border: 1px solid #8d8d8d; }
  386. @media (any-hover: none) { .dynamic-opacity { opacity: 0.8; } .dynamic-opacity:hover { opacity: 0.8; } }
  387. @media screen and (min-width: 767px) {
  388. aside.panel { display: contents; line-height: 1.5; }
  389. ul.outline { position: sticky; float: right; padding: 0 0 0 0.5em; margin: 0 0.5em -99vh; max-height: 80vh; border: 1px solid #BBBBBB; border-left: 2px solid #F2E5E5; box-shadow: 0 0 5px #ddd; background: linear-gradient(to right, #fcf1f1, #FFF 1em); list-style: none; width: 10.5%; color: gray; border-radius: 5px; overflow-y: scroll; z-index: 1; }
  390. ul.outline > li { overflow: hidden; text-overflow: ellipsis; }
  391. ul.outline > li > a { color: gray; white-space: nowrap; text-decoration: none; }
  392. }
  393. pre > code { overflow: hidden; display: block; }
  394. ul { padding-left: 1.5em; }
  395. .script-list > .regex-filtered { display: none; }
  396. #greasyfork-enhance-regex-filter-tip { float: right; color: grey; }
  397. @media screen and (max-width: 800px) { #greasyfork-enhance-regex-filter-tip { display: none; } }`);
  398. // Aside panel & Anchors
  399. const isScript = /^\/[^\/]+\/scripts/;
  400. const isSpecificScript = /^\/[^\/]+\/scripts\/\d+/;
  401. const isDisccussion = /^\/[^\/]+\/discussions/;
  402. const path = window.location.pathname;
  403. if ((!isScript.test(path) && !isDisccussion.test(path)) || isSpecificScript.test(path)) {
  404. const panel = body.insertBefore(document.createElement("aside"), $("body > div.width-constraint"));
  405. panel.className = "panel";
  406. const reference_node = $("body > div.width-constraint > section");
  407. const outline = panel.appendChild(document.createElement("ul"));
  408. outline.classList.add("outline");
  409. outline.classList.add("dynamic-opacity");
  410. outline.style.top = reference_node ? getComputedStyle(reference_node).marginTop : "1em";
  411. outline.style.marginTop = outline.style.top;
  412. let flag = false;
  413. $$("body > div.width-constraint h1, h2, h3, h4, h5, h6").forEach((node) => {
  414. flag = process(outline, node) || flag; // Not `flag || process(node)`!
  415. });
  416. if (!flag) {
  417. panel.remove();
  418. }
  419. }
  420. // Navigate to hash
  421. const hash = window.location.hash.slice(1);
  422. if (hash) {
  423. const ele = document.getElementById(decodeURIComponent(hash));
  424. if (ele) {
  425. ele.scrollIntoView();
  426. }
  427. }
  428. // Buttons
  429. const buttons = body.appendChild(document.createElement("div"));
  430. buttons.id = "float-buttons";
  431. const goToTop = buttons.appendChild(document.createElement("a"));
  432. goToTop.classList.add("button");
  433. goToTop.classList.add("dynamic-opacity");
  434. goToTop.href = "#top";
  435. goToTop.text = "↑";
  436. // Double click to get to top
  437. body.addEventListener("dblclick", (e) => {
  438. if (e.target === body) {
  439. goToTop.click();
  440. }
  441. });
  442. // Fix current tab link
  443. const tab = $("ul#script-links > li.current");
  444. if (tab) {
  445. const link = tab.appendChild(document.createElement("a"));
  446. link.href = window.location.pathname;
  447. link.appendChild(tab.firstChild);
  448. }
  449. const parts = window.location.pathname.split("/");
  450. if (parts.length <= 2 || (parts.length == 3 && parts[2] === '')) {
  451. const banner = $("header#main-header div#site-name");
  452. const img = banner.querySelector("img");
  453. const text = banner.querySelector("#site-name-text > h1");
  454. const link1 = document.createElement("a");
  455. link1.href = window.location.pathname;
  456. img.parentNode.replaceChild(link1, img);
  457. link1.appendChild(img);
  458. const link2 = document.createElement("a");
  459. link2.href = window.location.pathname;
  460. link2.textContent = text.textContent;
  461. text.textContent = "";
  462. text.appendChild(link2);
  463. }
  464. // Toolbar for code blocks
  465. const codeBlocks = document.getElementsByTagName("pre");
  466. for (const codeBlock of codeBlocks) {
  467. if (codeBlock.firstChild.tagName === "CODE") {
  468. const height = getComputedStyle(codeBlock.firstChild).getPropertyValue("height");
  469. codeBlock.firstChild.style.height = height;
  470. codeBlock.firstChild.setAttribute("data-height", height);
  471. codeBlock.insertAdjacentElement("afterbegin", createToolbar());
  472. }
  473. }
  474.  
  475. // Filter and Search
  476. // Shortcut
  477. function submitOnCtrlEnter(e) {
  478. const form = this.form;
  479. if (!form) return;
  480. // Ctrl + Enter to submit
  481. if (e.ctrlKey && e.key === "Enter") {
  482. form.submit();
  483. }
  484. }
  485. function handleInputFocus(e) {
  486. const ele = document.activeElement;
  487. // Ignore key combinations
  488. if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
  489. return;
  490. }
  491. // Do not interfere with input elements
  492. if (ele.tagName === "INPUT" || ele.tagName === "TEXTAREA" || ele.getAttribute("contenteditable") === "true") {
  493. if (e.key === "Escape") {
  494. e.preventDefault();
  495. ele.blur(); // Escape to blur
  496. }
  497. return;
  498. }
  499. // Do not interfere with input methods
  500. if (e.isComposing || e.keyCode === 229) {
  501. return;
  502. }
  503. if (e.key === "Enter") {
  504. const input = $("input[type=search]") || $("input[type=text]") || $("textarea");
  505. if (input) {
  506. e.preventDefault();
  507. input.focus();
  508. }
  509. }
  510. }
  511. let shortcutEnabled = false;
  512. function shortcut(enable) {
  513. const textAreas = $$("textarea");
  514. if (!shortcutEnabled && enable) {
  515. for (const textarea of textAreas) {
  516. textarea.addEventListener("keyup", submitOnCtrlEnter);
  517. }
  518. document.addEventListener("keydown", handleInputFocus);
  519. shortcutEnabled = true;
  520. } else if (shortcutEnabled && !enable) {
  521. for (const textarea of textAreas) {
  522. textarea.removeEventListener("keyup", submitOnCtrlEnter);
  523. }
  524. document.removeEventListener("keydown", handleInputFocus);
  525. shortcutEnabled = false;
  526. }
  527. }
  528. shortcut(configProxy["filterAndSearch.shortcut"]);
  529. // Regex filter
  530. const regexFilterTip = $(".sidebarred > .sidebarred-main-content > .script-list#browse-script-list")
  531. ?.previousElementSibling?.appendChild?.(document.createElement("span"));
  532. if (regexFilterTip) {
  533. regexFilterTip.id = idPrefix + "regex-filter-tip";
  534. regexFilterTip.title = `[${name}] Number of scripts filtered by regex`;
  535. }
  536. function setRegexFilterTip(content) {
  537. if (regexFilterTip) {
  538. regexFilterTip.textContent = content;
  539. }
  540. }
  541. function regexFilterOne(regex, script) {
  542. const info = script.querySelector("article > h2");
  543. if (!info) return;
  544. const name = info.querySelector(".script-link").textContent;
  545. const result = regex.test(name);
  546. script.classList.toggle("regex-filtered", result);
  547. if (result) {
  548. log("Filtered:", name);
  549. }
  550. return result;
  551. }
  552. function regexFilter(regexStr) {
  553. const debug = configProxy["other.debug"];
  554. const scripts = $$(".script-list > li");
  555. if (regexStr === "" || scripts.length === 0) {
  556. scripts.forEach(script => script.classList.remove("regex-filtered"));
  557. setRegexFilterTip("");
  558. return;
  559. }
  560. const regex = new RegExp(regexStr, "i");
  561. let count = 0;
  562. debug && console.groupCollapsed(`[${name}] Regex filtered scripts`);
  563. scripts.forEach(script => {
  564. if (regexFilterOne(regex, script)) {
  565. count++;
  566. }
  567. });
  568. setRegexFilterTip(`Filtered: ${count}/${scripts.length}`);
  569. debug && console.groupEnd();
  570. }
  571. regexFilter(configProxy["filterAndSearch.regexFilter"]);
  572. // Search syntax
  573. const types = {
  574. "script": "scripts",
  575. "lib": "scripts/libraries",
  576. "library": "scripts/libraries",
  577. // "code": "scripts/code-search", // It uses a different search parameter `c` instead of `q`
  578. "user": "users"
  579. };
  580. const langs = {
  581. "js": "",
  582. "javascript": "",
  583. "css": "css",
  584. "any": "all",
  585. "all": "all"
  586. };
  587. const sorts = {
  588. "rel": "",
  589. "relevant": "",
  590. "relevance": "",
  591. "day": "daily_installs",
  592. "daily": "daily_installs",
  593. "daily_install": "daily_installs",
  594. "daily_installs": "daily_installs",
  595. "total": "total_installs",
  596. "total_install": "total_installs",
  597. "total_installs": "total_installs",
  598. "score": "ratings",
  599. "rate": "ratings",
  600. "rating": "ratings",
  601. "ratings": "ratings",
  602. "created": "created",
  603. "created_at": "created",
  604. "updated": "updated",
  605. "updated_at": "updated",
  606. "name": "name",
  607. "title": "name",
  608. };
  609. if (configProxy["filterAndSearch.searchSyntax"]) {
  610. function parseString(input) {
  611. // Regular expression to match key:value pairs, allowing for non-word characters in values
  612. const regex = /\b(\w+:[^\s]+)\b/g;
  613. // Extract all key:value pairs
  614. const pairs = input.match(regex) || [];
  615. // Remove the pairs from the input string
  616. const cleanedString = input.replace(regex, '').replace(/\s{2,}/g, ' ').trim();
  617.  
  618. // Convert pairs to an object
  619. const parsedPairs = pairs.reduce((acc, pair) => {
  620. const [key, value] = pair.split(':');
  621. acc[key.toLowerCase()] = value.toLowerCase(); // Case-insensitive
  622. return acc;
  623. }, {});
  624.  
  625. return { cleanedString, parsedPairs };
  626. }
  627. function processSearch(search) {
  628. const form = search.form;
  629. if (form.method !== "get") {
  630. return;
  631. }
  632. form.addEventListener("submit", (e) => {
  633. const { cleanedString, parsedPairs } = parseString(search.value);
  634. if (cleanedString === search.value) return;
  635. search.value = cleanedString;
  636. if (!parsedPairs) return;
  637. e.preventDefault();
  638. const url = new URL(form.action, window.location.href);
  639. url.searchParams.set("q", cleanedString);
  640. if (parsedPairs["site"]) { // site:site-name
  641. url.pathname = `/scripts/by-site/${parsedPairs["site"]}`;
  642. } else if (parsedPairs["type"]) { // type:type, including "script", "lib"/"library", "code", "user"
  643. const typeUrl = types[parsedPairs["type"]];
  644. if (typeUrl) {
  645. url.pathname = `/${typeUrl}`;
  646. }
  647. }
  648. if (parsedPairs["lang"]) { // lang:language
  649. const lang = langs[parsedPairs["lang"]];
  650. if (lang === "") {
  651. url.searchParams.delete("language");
  652. } else if (lang) {
  653. url.searchParams.set("language", lang);
  654. }
  655. }
  656. if (parsedPairs["sort"]) { // sort:sort-by
  657. const sort = sorts[parsedPairs["sort"]];
  658. if (sort === "" || sort === "daily_installs" && cleanedString === "") {
  659. url.searchParams.delete("sort");
  660. } else if (sort) {
  661. url.searchParams.set("sort", sort);
  662. }
  663. }
  664. window.location.href = url.href;
  665. });
  666. }
  667. const searches = $$("input[type=search][name=q]");
  668. for (const search of searches) {
  669. processSearch(search);
  670. }
  671. }
  672.  
  673. // Code blocks
  674. // Auto hide code blocks
  675. function autoHide() {
  676. if (!configProxy["codeblocks.autoHideCode"]) {
  677. for (const code_block of codeBlocks) {
  678. const toggle = code_block.firstChild.lastChild;
  679. if (!toggle) continue;
  680. if (toggle.textContent === "Show code") {
  681. toggle.click(); // Click the toggle button
  682. }
  683. }
  684. } else {
  685. for (const codeBlock of codeBlocks) {
  686. const m = codeBlock.lastChild.textContent.match(/\n/g);
  687. const rows = m ? m.length : 0;
  688. const toggle = codeBlock.firstChild.lastChild;
  689. if (!toggle) continue;
  690. const hidden = toggle.textContent === "Show code";
  691. if (rows >= configProxy["codeblocks.autoHideRows"] && !hidden || rows < configProxy["codeblocks.autoHideRows"] && hidden) {
  692. codeBlock.firstChild.lastChild.click(); // Click the toggle button
  693. }
  694. }
  695. }
  696. }
  697. document.addEventListener("readystatechange", (e) => {
  698. if (e.target.readyState === "complete") {
  699. autoHide();
  700. }
  701. }, { once: true });
  702. // Tab size
  703. function tabSize(value) {
  704. const style = $("style#" + idPrefix + "tab-size") ?? document.head.appendChild(document.createElement("style"));
  705. style.id = idPrefix + "tab-size";
  706. style.textContent = `pre { tab-size: ${value}; }`;
  707. }
  708. tabSize(configProxy["codeblocks.tabSize"]);
  709. // Metadata
  710. function extractUserScriptMetadata(code) {
  711. const result = {};
  712. const userScriptRegex = /\/\/\s*=+\s*UserScript\s*=+\s*([\s\S]*?)\s*=+\s*\/UserScript\s*=+\s*/;
  713. const match = code.match(userScriptRegex);
  714. if (match) {// If the UserScript block is found
  715. const content = match[1];// Extract the content within the UserScript block
  716. const lines = content.split('\n'); // Split the content by newline
  717.  
  718. lines.forEach(line => {
  719. // Regular expression to match "// @name value" pattern
  720. const matchLine = line.trim().match(/^\/\/\s*@(\S+)\s+(.+)$/);
  721. if (matchLine) {
  722. const name = matchLine[1]; // Extract the name
  723. const value = matchLine[2]; // Extract the value
  724. switch (typeof result[name]) {
  725. case "undefined": // First occurrence
  726. result[name] = value;
  727. break;
  728. case "string": // Second occurrence
  729. result[name] = [result[name], value];
  730. break;
  731. case "object": // Third or more occurrence
  732. result[name].push(value);
  733. break;
  734. }
  735. }
  736. });
  737. }
  738. return result;
  739. }
  740. function metadata(enable) {
  741. const id = idPrefix + "metadata";
  742. const current = document.getElementById(id);
  743. if (current && !enable) {
  744. current.remove();
  745. } else if (!current && enable) {
  746. const scriptCodeBlock = document.querySelector(".code-container > pre.prettyprint.lang-js");
  747. const description = $("div#script-content");
  748. if (!window.location.pathname.endsWith("/code") || !scriptCodeBlock || !description) return;
  749. const metaBlock = document.createElement("ul");
  750. description.prepend(metaBlock);
  751. metaBlock.id = id;
  752. const script = scriptCodeBlock.querySelector("ol") ? Array.from(scriptCodeBlock.querySelectorAll("ol > li")).map(li => li.textContent).join("\n") : scriptCodeBlock.textContent;
  753. const metadata = extractUserScriptMetadata(script);
  754. const commonHosts = {
  755. GreasyFork: /^https?:\/\/update\.greasyfork\.org\/scripts\/\d+\/(?<ver>\d+)\/(?<name>.+?)\.js$/,
  756. JsDelivr: /^https?:\/\/cdn\.jsdelivr\.net\/(?<reg>\w+)\/(@[^/]+\/)?(?<name>[^@]+)@(?<ver>[^/]+)/,
  757. Cloudflare: /^https?:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/(?<name>[^/]+)\/(?<ver>[^/]+)/,
  758. };
  759. const commonRegistries = {
  760. npm: "NPM",
  761. gh: "GitHub",
  762. };
  763. // We're interested in `@grant`, `@connect`, `@require`, `@resource`
  764. const interestedMetadata = {};
  765. const interestedKeys = {
  766. grant: {
  767. brief: "Required permissions",
  768. display: (value) => {
  769. const valueCode = document.createElement("code");
  770. valueCode.textContent = value;
  771. if (value !== "none") {
  772. const valueLink = document.createElement("a");
  773. valueLink.appendChild(valueCode);
  774. valueLink.href = `https://www.tampermonkey.net/documentation.php#api:${valueCode.textContent}`;
  775. valueLink.title = `See documentation about ${valueCode.textContent}`;
  776. return valueLink;
  777. } else {
  778. return valueCode;
  779. }
  780. }
  781. },
  782. connect: {
  783. brief: "Allowed URLs to connect",
  784. display: (value) => {
  785. const valueCode = document.createElement("code");
  786. valueCode.textContent = value;
  787. return valueCode;
  788. }
  789. },
  790. require: {
  791. brief: "External libraries",
  792. display: (value) => {
  793. const valueLink = document.createElement("a");
  794. valueLink.href = value;
  795. valueLink.textContent = value;
  796. for (const [host, regex] of Object.entries(commonHosts)) {
  797. const match = value.match(regex);
  798. if (match) {
  799. const { name, ver, reg } = match.groups;
  800. const optionalRegistry = commonRegistries[reg] ? `${commonRegistries[reg]} on ` : "";
  801. valueLink.textContent = `${decodeURIComponent(name)}@${ver} (${optionalRegistry}${host})`;
  802. break;
  803. }
  804. }
  805. return valueLink;
  806. }
  807. },
  808. resource: {
  809. brief: "External resources",
  810. display: (value) => {
  811. const valueCode = document.createElement("code");
  812. const [name, link] = value.split(" ");
  813. const valueLink = document.createElement("a");
  814. valueLink.appendChild(valueCode);
  815. valueLink.href = link.trim();
  816. valueCode.textContent = name.trim();
  817. return valueLink;
  818. }
  819. }
  820. };
  821. for (const key in interestedKeys) {
  822. const values = metadata[key] ?? [];
  823. interestedMetadata[key] = Array.isArray(values) ? values : [values];
  824. }
  825. log("Interested Metadata:", interestedMetadata);
  826. // Display
  827. for (const [key, values] of Object.entries(interestedMetadata)) {
  828. const keyInfo = interestedKeys[key];
  829. const li = metaBlock.appendChild(document.createElement("li"));
  830. const keyLink = li.appendChild(document.createElement("a"));
  831. keyLink.href = `https://www.tampermonkey.net/documentation.php#meta:${key}`;
  832. keyLink.title = keyInfo.brief;
  833. keyLink.textContent = `@${key}`;
  834. const separator = li.appendChild(document.createElement("span"));
  835. separator.textContent = ": ";
  836. for (const value of values) {
  837. li.appendChild(keyInfo.display(value));
  838. const separator = li.appendChild(document.createElement("span"));
  839. separator.textContent = ", ";
  840. }
  841. if (values.length > 0) {
  842. li.lastChild.remove(); // Remove the last separator
  843. } else {
  844. li.appendChild(document.createTextNode("none"));
  845. }
  846. }
  847. }
  848. }
  849. metadata(configProxy["codeblocks.metadata"]);
  850.  
  851. // Display
  852. // Flat layout
  853. function flatLayout(enable) {
  854. const meta_orig = $("#script-info > #script-content .script-meta-block");
  855. const meta_mod = $("#script-info > .script-meta-block");
  856. if (enable && meta_orig) {
  857. const header = $("#script-info > header");
  858. header.before(meta_orig);
  859. } else if (!enable && meta_mod) {
  860. const additional = $("#script-info > #script-content #additional-info");
  861. additional.before(meta_mod);
  862. }
  863. }
  864. flatLayout(configProxy["display.flatLayout"]);
  865. // Always show notification
  866. function alwaysShowNotification(enable) {
  867. const nav = $("#nav-user-info");
  868. const profile = nav?.querySelector(".user-profile-link");
  869. const existing = nav.querySelector(".notification-widget");
  870. if (!nav || !profile || existing && existing.textContent !== "0") return; // There's unread notification or user is not logged in
  871. if (enable && !existing) {
  872. const notification = nav.insertBefore(document.createElement("a"), profile);
  873. notification.className = "notification-widget";
  874. notification.textContent = "0";
  875. notification.href = profile.querySelector("a").href + "/notifications";
  876. } else if (!enable && existing) {
  877. existing.remove();
  878. }
  879. }
  880. alwaysShowNotification(configProxy["display.alwaysShowNotification"]);
  881.  
  882. // Other
  883. // Short link
  884. function shortLink(enable) {
  885. const description = $("div#script-content");
  886. const url = window.location.href;
  887. const scriptId = url.match(/\/scripts\/(\d+)/)?.[1];
  888. if (!scriptId || !description) return;
  889. const id = idPrefix + "short-link";
  890. const current = document.getElementById(id);
  891. if (current && !enable) {
  892. current.remove();
  893. } else if (!current && enable) {
  894. const short = `https://greasyfork.org/scripts/${scriptId}`;
  895. const p = description.insertAdjacentElement("beforebegin", document.createElement("p"));
  896. p.id = id;
  897. p.textContent = "Short link: ";
  898. const link = p.appendChild(document.createElement("a"));
  899. link.href = short;
  900. link.textContent = short;
  901. const copy = p.appendChild(document.createElement("a"));
  902. copy.textContent = "(Copy)";
  903. copy.style.marginLeft = "1em";
  904. copy.style.cursor = "pointer";
  905. copy.title = "Copy short link to clipboard";
  906. copy.addEventListener("click", () => {
  907. if (copy.textContent === "(Copied!)") return;
  908. navigator.clipboard.writeText(short).then(() => {
  909. copy.textContent = "(Copied!)";
  910. window.setTimeout(() => {
  911. copy.textContent = "(Copy)";
  912. }, 1000);
  913. });
  914. });
  915. }
  916. }
  917. shortLink(configProxy["other.shortLink"]);
  918. // Alternative URLs for library
  919. function alternativeURLs(enable) {
  920. if ($(".remove-attachments") || !$("div#script-content") || $("div#script-content > div#install-area")) return; // Not a library
  921. const id = idPrefix + "lib-alternative-url";
  922. const current = document.getElementById(id);
  923. if (current && !enable) {
  924. current.remove();
  925. } else if (!current && enable) {
  926. const description = $("div#script-content > p");
  927. const trim = "// @require ";
  928. const text = description?.querySelector("code")?.textContent;
  929. if (!text || !text.startsWith(trim)) return; // Found no URL
  930. const url = text.slice(trim.length);
  931. const parts = url.split("/");
  932. const scriptId = parts[4];
  933. const scriptVersion = parts[5];
  934. const fileName = parts[6];
  935. const URLs = [
  936. [`// @require https://update.greasyfork.org/scripts/${scriptId}/${fileName}`, "Latest version"],
  937. [`// @require https://greasyfork.org/scripts/${scriptId}/code/${fileName}?version=${scriptVersion}`, "Current version (Legacy)"],
  938. [`// @require https://greasyfork.org/scripts/${scriptId}/code/${fileName}`, "Latest version (Legacy)"],
  939. ];
  940.  
  941. const detail = document.createElement("p").appendChild(document.createElement("details"));
  942. description.after(detail.parentElement);
  943. detail.parentElement.id = id;
  944. detail.appendChild(document.createElement("summary")).textContent = "Alternative URLs";
  945. const list = detail.appendChild(document.createElement("ul"));
  946. for (const [url, text] of URLs) {
  947. const link = list.appendChild(document.createElement("li")).appendChild(document.createElement("code"));
  948. link.textContent = url;
  949. link.title = text;
  950. }
  951. }
  952. }
  953. alternativeURLs(configProxy["other.libAlternativeUrl"]);
  954. // Image proxy
  955. if (configProxy["other.imageProxy"]) {
  956. const PROXY = "https://wsrv.nl/?url=";
  957. const images = $$("a[href^='/rails/active_storage/blobs/redirect/'] > img[src^='https://greasyfork.']");
  958. for (const img of images) {
  959. img.src = PROXY + img.src;
  960. const link = img.parentElement;
  961. link.href = PROXY + link.href;
  962. }
  963. }
  964. // Lazy image
  965. if (configProxy["other.lazyImage"]) {
  966. const images = $$(".user-content img");
  967. for (const image of images) {
  968. image.loading = "lazy";
  969. }
  970. }
  971.  
  972. // Initialize css
  973. for (const prop in dynamicStyle) {
  974. cssHelper(prop, configProxy[prop]);
  975. }
  976. // Dynamically respond to config changes
  977. const callbacks = {
  978. "filterAndSearch.shortcut": shortcut,
  979. "filterAndSearch.regexFilter": regexFilter,
  980. "codeblocks.autoHideCode": autoHide,
  981. "codeblocks.autoHideRows": autoHide,
  982. "codeblocks.tabSize": tabSize,
  983. "codeblocks.metadata": metadata,
  984. "display.flatLayout": flatLayout,
  985. "display.alwaysShowNotification": alwaysShowNotification,
  986. "other.shortLink": shortLink,
  987. "other.libAlternativeUrl": alternativeURLs,
  988. };
  989. config.addEventListener("set", e => {
  990. const callback = callbacks[e.detail.prop];
  991. if (callback && (e.detail.before !== e.detail.after)) {
  992. callback(e.detail.after);
  993. }
  994. if (e.detail.prop in dynamicStyle) {
  995. cssHelper(e.detail.prop, e.detail.after);
  996. }
  997. });
  998. })();