Greasy Fork 增强

增进 Greasyfork 浏览体验。

当前为 2024-06-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Greasy Fork Enhance
  3. // @name:zh-CN Greasy Fork 增强
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.7.7
  6. // @description Enhance your experience at Greasyfork.
  7. // @description:zh-CN 增进 Greasyfork 浏览体验。
  8. // @author PRO
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_deleteValue
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_unregisterMenuCommand
  14. // @match https://greasyfork.org/*
  15. // @require https://update.greasyfork.org/scripts/470224/1317473/Tampermonkey%20Config.js
  16. // @icon https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/public/images/blacklogo16.png
  17. // @icon64 https://raw.githubusercontent.com/greasyfork-org/greasyfork/main/public/images/blacklogo96.png
  18. // @license gpl-3.0
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23. // Judge if the script should run
  24. const no_run = [".json", ".js"];
  25. let is_run = true;
  26. const idPrefix = "greasyfork-enhance-";
  27. no_run.forEach((suffix) => {
  28. if (window.location.pathname.endsWith(suffix)) {
  29. is_run = false;
  30. }
  31. });
  32. if (!is_run) return;
  33. // Config
  34. const config_desc = {
  35. "$default": {
  36. value: true,
  37. input: "current",
  38. processor: "not",
  39. formatter: "boolean",
  40. autoClose: false
  41. },
  42. "auto-hide-code": { name: "Auto hide code", title: "Hide long code blocks by default" },
  43. "auto-hide-rows": {
  44. name: "Min rows to hide",
  45. value: 10,
  46. input: "prompt",
  47. processor: "int_range-1-",
  48. formatter: "normal",
  49. title: "Minimum number of rows to hide"
  50. },
  51. "hide-buttons": { name: "Hide buttons", title: "Hide floating buttons added by this script", value: false },
  52. "flat-layout": { name: "Flat layout", title: "Use flat layout for script list and descriptions", value: false },
  53. "animation": { name: "Animation", title: "Enable animation for toggling code blocks" },
  54. "lib-alternative-url": { name: "Alternative URLs for library", title: "Show a list of alternative URLs for a given library", value: false },
  55. "short-link": { name: "Short link", title: "Display a shortened link to current script" },
  56. "shortcut": { name: "Shortcut", title: "Enable keyboard shortcuts" },
  57. "search-syntax": { name: "*Search syntax", title: "Enable partial search syntax for Greasy Fork search bar" },
  58. "image-proxy": { name: "*Image proxy", title: "Use `wsrv.nl` as proxy for user-uploaded images", value: false },
  59. };
  60. const config = GM_config(config_desc);
  61. // CSS
  62. const dynamicStyle = {
  63. "hide-buttons": `div#float-buttons { display: none; }`,
  64. "flat-layout": `
  65. .script-list > li {
  66. &:not(.ad-entry) { padding-right: 0; }
  67. article {
  68. display: flex; flex-direction: row; justify-content: space-between; align-items: center;
  69. > .script-meta-block {
  70. width: 40%; column-gap: 0;
  71. > .inline-script-stats {
  72. margin: 0;
  73. > dd { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  74. }
  75. }
  76. > h2 {
  77. width: 60%; overflow: hidden; text-overflow: ellipsis; margin-right: 0.5em; padding-right: 0.5em; border-right: 1px solid #88888888;
  78. > .script-link { white-space: nowrap; }
  79. > .script-description { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  80. }
  81. }
  82. &[data-script-type="library"] > article {
  83. > h2 { width: 80%; }
  84. > .script-meta-block { width: 20%; column-count: 1; }
  85. }
  86. }
  87. @media (max-width: 600px) {
  88. .script-list > li {
  89. &[data-script-type="library"] > article > div.script-meta-block { width: 40%; }
  90. &:not([data-script-type="library"]) > article {
  91. display: block;
  92. > h2 { width: unset; border-right: none; }
  93. > .script-meta-block { column-count: 2; }
  94. }
  95. > article > div.script-meta-block { width: unset; column-gap: 0; }
  96. }
  97. }
  98. .showing-all-languages .badge-js, .showing-all-languages .badge-css, .script-type { display: none; }
  99. #script-info .script-meta-block { float: right; column-count: 1; max-width: 300px; border-left: 1px solid #DDDDDD; margin-left: 1em; padding-left: 1em; }
  100. #additional-info { width: calc(100% - 2em - 2px); }
  101. `,
  102. "animation": `
  103. /* Toggle code animation */
  104. pre > code { transition: height 0.5s ease-in-out 0s; }
  105. /* Adapted from animate.css - https://animate.style/ */
  106. :root { --animate-duration: 1s; --animate-delay: 1s; --animate-repeat: 1; }
  107. .animate__animated { animation-duration: var(--animate-duration); animation-fill-mode: both; }
  108. .animate__animated.animate__fastest { animation-duration: calc(var(--animate-duration) / 3); }
  109. @keyframes tada {
  110. from { transform: scale3d(1, 1, 1); }
  111. 10%, 20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
  112. 30%, 50%, 70%, 90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
  113. 40%, 60%, 80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
  114. to { transform: scale3d(1, 1, 1); }
  115. }
  116. .animate__tada { animation-name: tada; }
  117. @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  118. .animate__fadeIn { animation-name: fadeIn; }
  119. @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
  120. .animate__fadeOut { -webkit-animation-name: fadeOut; animation-name: fadeOut; }
  121. `
  122. };
  123. // Functions
  124. const $ = document.querySelector.bind(document);
  125. const $$ = document.querySelectorAll.bind(document);
  126. const body = $("body");
  127. function sanitify(s) {
  128. // Remove emojis (such a headache)
  129. s = s.replaceAll(/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDEFF]|\uFE0F)/g, "");
  130. // Trim spaces and newlines
  131. s = s.trim();
  132. // Replace spaces
  133. s = s.replaceAll(" ", "-");
  134. s = s.replaceAll("%20", "-");
  135. // No more multiple "-"
  136. s = s.replaceAll(/-+/g, "-");
  137. return s;
  138. }
  139. function process(node) { // Add anchor and assign id to given node; Add to outline. Return true if node is actually processed.
  140. if (node.childElementCount > 1 || node.classList.length > 0) return false; // Ignore complex nodes
  141. const text = node.textContent;
  142. if (!node.id) { // If the node has no id
  143. node.id = sanitify(text); // Then assign id
  144. }
  145. // Add anchors
  146. const anchor = node.appendChild(document.createElement('a'));
  147. anchor.className = 'anchor';
  148. anchor.href = '#' + node.id;
  149. const link = outline.appendChild(document.createElement("li"))
  150. .appendChild(document.createElement("a"));
  151. link.href = "#" + node.id;
  152. link.text = text;
  153. return true;
  154. }
  155. async function animate(node, animation) {
  156. return new Promise((resolve, reject) => {
  157. node.classList.add("animate__animated", "animate__" + animation);
  158. if (node.getAnimations().length == 0) {
  159. node.classList.remove("animate__animated", "animate__" + animation);
  160. reject("No animation available");
  161. }
  162. node.addEventListener('animationend', e => {
  163. e.stopPropagation();
  164. node.classList.remove("animate__animated", "animate__" + animation);
  165. resolve("Animation ended");
  166. }, { once: true });
  167. });
  168. }
  169. async function transition(node, height) {
  170. return new Promise((resolve, reject) => {
  171. node.style.height = height;
  172. if (node.getAnimations().length == 0) {
  173. resolve("No transition available");
  174. }
  175. node.addEventListener('transitionend', e => {
  176. e.stopPropagation();
  177. resolve("Transition ended");
  178. }, { once: true });
  179. });
  180. }
  181. function copyCode() {
  182. const code = this.parentNode.nextElementSibling;
  183. const text = code.textContent;
  184. navigator.clipboard.writeText(text).then(() => {
  185. this.textContent = "Copied!";
  186. animate(this, "tada").then(() => {
  187. this.textContent = "Copy code";
  188. }, () => {
  189. window.setTimeout(() => {
  190. this.textContent = "Copy code";
  191. }, 1000);
  192. });
  193. });
  194. }
  195. function toggleCode() {
  196. const code = this.parentNode.nextElementSibling;
  197. if (code.style.height == "0px") {
  198. code.style.willChange = "height";
  199. transition(code, code.getAttribute("data-height")).then(() => {
  200. code.style.willChange = "";
  201. });
  202. animate(this, "fadeOut").then(() => {
  203. this.textContent = "Hide code";
  204. animate(this, "fadeIn");
  205. }, () => {
  206. this.textContent = "Hide code";
  207. });
  208. } else {
  209. code.style.willChange = "height";
  210. transition(code, "0px").then(() => {
  211. code.style.willChange = "";
  212. });
  213. animate(this, "fadeOut").then(() => {
  214. this.textContent = "Show code";
  215. animate(this, "fadeIn");
  216. }, () => {
  217. this.textContent = "Show code";
  218. });
  219. }
  220. }
  221. function create_toolbar() {
  222. const toolbar = document.createElement("div");
  223. const copy = toolbar.appendChild(document.createElement("a"));
  224. const toggle = toolbar.appendChild(document.createElement("a"));
  225. copy.textContent = "Copy code";
  226. copy.className = "code-operation";
  227. copy.title = "Copy code to clipboard";
  228. copy.addEventListener("click", copyCode);
  229. toggle.textContent = "Hide code";
  230. toggle.classList.add("code-operation", "animate__fastest");
  231. toggle.title = "Toggle code display";
  232. toggle.addEventListener("click", toggleCode);
  233. // Css
  234. toolbar.className = "code-toolbar";
  235. return toolbar;
  236. }
  237. function injectCSS(id, css) {
  238. const style = document.head.appendChild(document.createElement("style"));
  239. style.id = idPrefix + id;
  240. style.textContent = css;
  241. }
  242. function cssHelper(id, enable) {
  243. const current = document.getElementById(idPrefix + id);
  244. if (current) {
  245. current.disabled = !enable;
  246. } else if (enable) {
  247. injectCSS(id, dynamicStyle[id]);
  248. }
  249. }
  250. // Basic css
  251. injectCSS("basic", `
  252. html { scroll-behavior: smooth; }
  253. a.anchor::before { content: "#"; }
  254. a.anchor { opacity: 0; text-decoration: none; padding: 0px 0.5em; transition: all 0.25s ease-in-out; }
  255. h1:hover>a.anchor, h2:hover>a.anchor, h3:hover>a.anchor,
  256. h4:hover>a.anchor, h5:hover>a.anchor, h6:hover>a.anchor { opacity: 1; transition: all 0.25s ease-in-out; }
  257. 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; }
  258. div.code-toolbar { display: flex; gap: 1em; }
  259. a.code-operation { cursor: pointer; font-style: italic; }
  260. div.lum-lightbox { z-index: 2; }
  261. #float-buttons { position: fixed; bottom: 1em; right: 1em; display: flex; flex-direction: column; user-select: none; z-index: 1; }
  262. aside.panel { display: none; }
  263. .dynamic-opacity { transition: opacity 0.2s ease-in-out; opacity: 0.2; }
  264. .dynamic-opacity:hover { opacity: 0.8; }
  265. 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; }
  266. input[type=file]:hover { border-color: black; background: rgba(169, 169, 169, 0.6); }
  267. 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); }
  268. input[type=file]::file-selector-button:hover { background: rgba(169, 169, 169, 1); }
  269. table { border: 1px solid #8d8d8d; border-collapse: collapse; width: auto; }
  270. table td, table th { padding: 0.5em 0.75em; vertical-align: middle; border: 1px solid #8d8d8d; }
  271. @media (any-hover: none) { .dynamic-opacity { opacity: 0.8; } .dynamic-opacity:hover { opacity: 0.8; } }
  272. @media screen and (min-width: 767px) {
  273. aside.panel { display: contents; line-height: 1.5; }
  274. 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; }
  275. ul.outline > li { overflow: hidden; text-overflow: ellipsis; }
  276. ul.outline > li > a { color: gray; white-space: nowrap; text-decoration: none; }
  277. }
  278. pre > code { overflow: hidden; display: block; }
  279. ul { padding-left: 1.5em; }`);
  280. // Aside panel & Anchors
  281. let outline;
  282. const is_script = /^\/[^\/]+\/scripts/;
  283. const is_specific_script = /^\/[^\/]+\/scripts\/\d+/;
  284. const is_disccussion = /^\/[^\/]+\/discussions/;
  285. const path = window.location.pathname;
  286. if ((!is_script.test(path) && !is_disccussion.test(path)) || is_specific_script.test(path)) {
  287. const panel = body.insertBefore(document.createElement("aside"), $("body > div.width-constraint"));
  288. panel.className = "panel";
  289. const reference_node = $("body > div.width-constraint > section");
  290. outline = panel.appendChild(document.createElement("ul"));
  291. outline.classList.add("outline");
  292. outline.classList.add("dynamic-opacity");
  293. outline.style.top = reference_node ? getComputedStyle(reference_node).marginTop : "1em";
  294. outline.style.marginTop = outline.style.top;
  295. let flag = false;
  296. $$("body > div.width-constraint h1, h2, h3, h4, h5, h6").forEach((node) => {
  297. flag = process(node) || flag; // Not `flag || process(node)`!
  298. });
  299. if (!flag) {
  300. panel.remove();
  301. }
  302. }
  303. // Navigate to hash
  304. const hash = window.location.hash.slice(1);
  305. if (hash) {
  306. const ele = document.getElementById(decodeURIComponent(hash));
  307. if (ele) {
  308. ele.scrollIntoView();
  309. }
  310. }
  311. // Buttons
  312. const buttons = body.appendChild(document.createElement("div"));
  313. buttons.id = "float-buttons";
  314. const to_top = buttons.appendChild(document.createElement("a"));
  315. to_top.classList.add("button");
  316. to_top.classList.add("dynamic-opacity");
  317. to_top.href = "#top";
  318. to_top.text = "↑";
  319. // Double click to get to top
  320. body.addEventListener("dblclick", (e) => {
  321. if (e.target === body) {
  322. to_top.click();
  323. }
  324. });
  325. // Fix current tab link
  326. const tab = $("ul#script-links > li.current");
  327. if (tab) {
  328. const link = tab.appendChild(document.createElement("a"));
  329. link.href = window.location.pathname;
  330. link.appendChild(tab.firstChild);
  331. }
  332. const parts = window.location.pathname.split("/");
  333. if (parts.length <= 2 || (parts.length == 3 && parts[2] === '')) {
  334. const banner = $("header#main-header div#site-name");
  335. const img = banner.querySelector("img");
  336. const text = banner.querySelector("#site-name-text > h1");
  337. const link1 = document.createElement("a");
  338. link1.href = window.location.pathname;
  339. img.parentNode.replaceChild(link1, img);
  340. link1.appendChild(img);
  341. const link2 = document.createElement("a");
  342. link2.href = window.location.pathname;
  343. link2.textContent = text.textContent;
  344. text.textContent = "";
  345. text.appendChild(link2);
  346. }
  347. // Toolbar for code blocks
  348. const code_blocks = document.getElementsByTagName("pre");
  349. for (const code_block of code_blocks) {
  350. if (code_block.firstChild.tagName === "CODE") {
  351. const height = getComputedStyle(code_block.firstChild).getPropertyValue("height");
  352. code_block.firstChild.style.height = height;
  353. code_block.firstChild.setAttribute("data-height", height);
  354. code_block.insertAdjacentElement("afterbegin", create_toolbar());
  355. }
  356. }
  357. // Auto hide code blocks
  358. function autoHide() {
  359. if (!config["auto-hide-code"]) {
  360. for (const code_block of code_blocks) {
  361. const toggle = code_block.firstChild.lastChild;
  362. if (toggle.textContent === "Show code") {
  363. toggle.click(); // Click the toggle button
  364. }
  365. }
  366. } else {
  367. for (const code_block of code_blocks) {
  368. const m = code_block.lastChild.textContent.match(/\n/g);
  369. const rows = m ? m.length : 0;
  370. const toggle = code_block.firstChild.lastChild;
  371. const hidden = toggle.textContent === "Show code";
  372. if (rows >= config["auto-hide-rows"] && !hidden || rows < config["auto-hide-rows"] && hidden) {
  373. code_block.firstChild.lastChild.click(); // Click the toggle button
  374. }
  375. }
  376. }
  377. }
  378. document.addEventListener("readystatechange", (e) => {
  379. if (e.target.readyState === "complete") {
  380. autoHide();
  381. }
  382. }, { once: true });
  383. // Alternative URLs for library
  384. function alternativeURLs(enable) {
  385. if ($(".remove-attachments") || !$("div#script-content") || $("div#script-content > div#install-area")) return; // Not a library
  386. const id = idPrefix + "lib-alternative-url";
  387. const current = document.getElementById(id);
  388. if (current && !enable) {
  389. current.remove();
  390. } else if (!current && enable) {
  391. const description = $("div#script-content > p");
  392. const trim = "// @require ";
  393. const text = description?.querySelector("code")?.textContent;
  394. if (!text || !text.startsWith(trim)) return; // Found no URL
  395. const url = text.slice(trim.length);
  396. const parts = url.split("/");
  397. const scriptId = parts[4];
  398. const scriptVersion = parts[5];
  399. const fileName = parts[6];
  400. const URLs = [
  401. [`// @require https://update.greasyfork.org/scripts/${scriptId}/${fileName}`, "Latest version"],
  402. [`// @require https://greasyfork.org/scripts/${scriptId}/code/${fileName}?version=${scriptVersion}`, "Current version (Legacy)"],
  403. [`// @require https://greasyfork.org/scripts/${scriptId}/code/${fileName}`, "Latest version (Legacy)"],
  404. ];
  405.  
  406. const detail = document.createElement("p").appendChild(document.createElement("details"));
  407. description.after(detail.parentElement);
  408. detail.parentElement.id = id;
  409. detail.appendChild(document.createElement("summary")).textContent = "Alternative URLs";
  410. const list = detail.appendChild(document.createElement("ul"));
  411. for (const [url, text] of URLs) {
  412. const link = list.appendChild(document.createElement("li")).appendChild(document.createElement("code"));
  413. link.textContent = url;
  414. link.title = text;
  415. }
  416. }
  417. }
  418. alternativeURLs(config["lib-alternative-url"]);
  419. // Short link
  420. function shortLink(enable) {
  421. const description = $("div#script-content");
  422. const url = window.location.href;
  423. const scriptId = url.match(/\/scripts\/(\d+)/)?.[1];
  424. if (!scriptId || !description) return;
  425. const id = idPrefix + "short-link";
  426. const current = document.getElementById(id);
  427. if (current && !enable) {
  428. current.remove();
  429. } else if (!current && enable) {
  430. const short = `https://greasyfork.org/scripts/${scriptId}`;
  431. const p = description.insertAdjacentElement("beforebegin", document.createElement("p"));
  432. p.id = id;
  433. p.textContent = "Short link: ";
  434. const link = p.appendChild(document.createElement("a"));
  435. link.href = short;
  436. link.textContent = short;
  437. const copy = p.appendChild(document.createElement("a"));
  438. copy.textContent = "(Copy)";
  439. copy.style.marginLeft = "1em";
  440. copy.style.cursor = "pointer";
  441. copy.title = "Copy short link to clipboard";
  442. copy.addEventListener("click", () => {
  443. if (copy.textContent === "(Copied!)") return;
  444. navigator.clipboard.writeText(short).then(() => {
  445. copy.textContent = "(Copied!)";
  446. window.setTimeout(() => {
  447. copy.textContent = "(Copy)";
  448. }, 1000);
  449. });
  450. });
  451. }
  452. }
  453. shortLink(config["short-link"]);
  454. // Shortcut
  455. function submitOnCtrlEnter(e) {
  456. const form = this.form;
  457. if (!form) return;
  458. // Ctrl + Enter to submit
  459. if (e.ctrlKey && e.key === "Enter") {
  460. form.submit();
  461. }
  462. }
  463. function handleInputFocus(e) {
  464. const ele = document.activeElement;
  465. // Ignore key combinations
  466. if (e.ctrlKey || e.altKey || e.metaKey || e.shiftKey) {
  467. return;
  468. }
  469. // Do not interfere with input elements
  470. if (ele.tagName === "INPUT" || ele.tagName === "TEXTAREA" || ele.getAttribute("contenteditable") === "true") {
  471. if (e.key === "Escape") {
  472. e.preventDefault();
  473. ele.blur(); // Escape to blur
  474. }
  475. return;
  476. }
  477. // Do not interfere with input methods
  478. if (e.isComposing || e.keyCode === 229) {
  479. return;
  480. }
  481. if (e.key === "Enter") {
  482. const input = $("input[type=search]") || $("input[type=text]") || $("textarea");
  483. if (input) {
  484. e.preventDefault();
  485. input.focus();
  486. }
  487. }
  488. }
  489. let shortcutEnabled = false;
  490. function shortcut(enable) {
  491. const textAreas = $$("textarea");
  492. if (!shortcutEnabled && enable) {
  493. for (const textarea of textAreas) {
  494. textarea.addEventListener("keyup", submitOnCtrlEnter);
  495. }
  496. document.addEventListener("keydown", handleInputFocus);
  497. shortcutEnabled = true;
  498. } else if (shortcutEnabled && !enable) {
  499. for (const textarea of textAreas) {
  500. textarea.removeEventListener("keyup", submitOnCtrlEnter);
  501. }
  502. document.removeEventListener("keydown", handleInputFocus);
  503. shortcutEnabled = false;
  504. }
  505. }
  506. shortcut(config["shortcut"]);
  507. // Initialize css
  508. for (const prop in dynamicStyle) {
  509. cssHelper(prop, config[prop]);
  510. }
  511. // Dynamically respond to config changes
  512. const callbacks = {
  513. "auto-hide-code": autoHide,
  514. "auto-hide-rows": autoHide,
  515. "flat-layout": (after) => {
  516. const meta_orig = $("#script-info > #script-content > .script-meta-block");
  517. const meta_mod = $("#script-info > .script-meta-block");
  518. if (after && meta_orig) {
  519. const links = $("#script-info > #script-links");
  520. links.after(meta_orig);
  521. } else if (!after && meta_mod) {
  522. const additional = $("#script-info > #script-content > #additional-info");
  523. additional.before(meta_mod);
  524. }
  525. },
  526. "lib-alternative-url": alternativeURLs,
  527. "short-link": shortLink,
  528. "shortcut": shortcut,
  529. };
  530. callbacks["flat-layout"](config["flat-layout"]);
  531. window.addEventListener(GM_config_event, e => {
  532. if (e.detail.type === "set") {
  533. const callback = callbacks[e.detail.prop];
  534. if (callback && (e.detail.before !== e.detail.after)) {
  535. callback(e.detail.after);
  536. }
  537. if (e.detail.prop in dynamicStyle) {
  538. cssHelper(e.detail.prop, e.detail.after);
  539. }
  540. }
  541. });
  542. // Search syntax
  543. const types = {
  544. "script": "scripts",
  545. "lib": "scripts/libraries",
  546. "library": "scripts/libraries",
  547. // "code": "scripts/code-search", // It uses a different search parameter `c` instead of `q`
  548. "user": "users"
  549. };
  550. const langs = {
  551. "js": "",
  552. "javascript": "",
  553. "css": "css",
  554. "any": "all",
  555. "all": "all"
  556. };
  557. const sorts = {
  558. "rel": "",
  559. "relevant": "",
  560. "relevance": "",
  561. "day": "daily_installs",
  562. "daily": "daily_installs",
  563. "daily_install": "daily_installs",
  564. "daily_installs": "daily_installs",
  565. "total": "total_installs",
  566. "total_install": "total_installs",
  567. "total_installs": "total_installs",
  568. "score": "ratings",
  569. "rate": "ratings",
  570. "rating": "ratings",
  571. "ratings": "ratings",
  572. "created": "created",
  573. "created_at": "created",
  574. "updated": "updated",
  575. "updated_at": "updated",
  576. "name": "name",
  577. "title": "name",
  578. };
  579. if (config["search-syntax"]) {
  580. function parseString(input) {
  581. // Regular expression to match key:value pairs, allowing for non-word characters in values
  582. const regex = /\b(\w+:[^\s]+)\b/g;
  583. // Extract all key:value pairs
  584. const pairs = input.match(regex) || [];
  585. // Remove the pairs from the input string
  586. const cleanedString = input.replace(regex, '').replace(/\s{2,}/g, ' ').trim();
  587.  
  588. // Convert pairs to an object
  589. const parsedPairs = pairs.reduce((acc, pair) => {
  590. const [key, value] = pair.split(':');
  591. acc[key.toLowerCase()] = value.toLowerCase(); // Case-insensitive
  592. return acc;
  593. }, {});
  594.  
  595. return { cleanedString, parsedPairs };
  596. }
  597. function processSearch(search) {
  598. const form = search.form;
  599. if (form.method !== "get") {
  600. return;
  601. }
  602. form.addEventListener("submit", (e) => {
  603. const { cleanedString, parsedPairs } = parseString(search.value);
  604. if (cleanedString === search.value) return;
  605. search.value = cleanedString;
  606. if (!parsedPairs) return;
  607. e.preventDefault();
  608. const url = new URL(form.action, window.location.href);
  609. url.searchParams.set("q", cleanedString);
  610. if (parsedPairs["site"]) { // site:site-name
  611. url.pathname = `/scripts/by-site/${parsedPairs["site"]}`;
  612. } else if (parsedPairs["type"]) { // type:type, including "script", "lib"/"library", "code", "user"
  613. const typeUrl = types[parsedPairs["type"]];
  614. if (typeUrl) {
  615. url.pathname = `/${typeUrl}`;
  616. }
  617. }
  618. if (parsedPairs["lang"]) { // lang:language
  619. const lang = langs[parsedPairs["lang"]];
  620. if (lang === "") {
  621. url.searchParams.delete("language");
  622. } else if (lang) {
  623. url.searchParams.set("language", lang);
  624. }
  625. }
  626. if (parsedPairs["sort"]) { // sort:sort-by
  627. const sort = sorts[parsedPairs["sort"]];
  628. if (sort === "" || sort === "daily_installs" && cleanedString === "") {
  629. url.searchParams.delete("sort");
  630. } else if (sort) {
  631. url.searchParams.set("sort", sort);
  632. }
  633. }
  634. window.location.href = url.href;
  635. });
  636. }
  637. const searches = $$("input[type=search][name=q]");
  638. for (const search of searches) {
  639. processSearch(search);
  640. }
  641. }
  642. // Image proxy
  643. if (config["image-proxy"]) {
  644. const PROXY = "https://wsrv.nl/?url=";
  645. const images = $$("a[href^='/rails/active_storage/blobs/redirect/'] > img[src^='https://greasyfork.']");
  646. for (const img of images) {
  647. img.src = PROXY + img.src;
  648. const link = img.parentElement;
  649. link.href = PROXY + link.href;
  650. }
  651. }
  652. })();