Greasy Fork 增强

增进 Greasyfork 浏览体验。

目前为 2024-06-15 提交的版本。查看 最新版本

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