Greasy Fork 增强

增进 Greasyfork 浏览体验。

当前为 2024-12-08 提交的版本,查看 最新版本

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