Greasy Fork 增强

增进 Greasyfork 浏览体验。

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

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