Greasy Fork Enhance

Enhance your experience at Greasyfork.

目前為 2024-10-06 提交的版本,檢視 最新版本

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