Greasy Fork 增强

增进 Greasyfork 浏览体验。

当前为 2023-12-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Greasy Fork Enhance
  3. // @name:zh-CN Greasy Fork 增强
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.6.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://greasyfork.org/scripts/470224-tampermonkey-config/code/Tampermonkey%20Config.js?version=1244657
  16. // @icon https://greasyfork.org/vite/assets/blacklogo16-bc64b9f7.png
  17. // @license gpl-3.0
  18. // ==/UserScript==
  19.  
  20. (function () {
  21. 'use strict';
  22. // Judge if the script should run
  23. const no_run = [".json", ".js"];
  24. let is_run = true;
  25. const idPrefix = "greasyfork-enhance-";
  26. no_run.forEach((suffix) => {
  27. if (window.location.pathname.endsWith(suffix)) {
  28. is_run = false;
  29. }
  30. });
  31. if (!is_run) return;
  32. // Config
  33. function _boolDesc(name, title = undefined, default_value = true) {
  34. return {
  35. name: name,
  36. value: default_value,
  37. input: "current",
  38. processor: "not",
  39. formatter: "boolean",
  40. autoClose: false,
  41. title: title
  42. };
  43. }
  44. const config_desc = {
  45. "auto-hide-code": _boolDesc("Auto hide code", "Hide long code blocks by default"),
  46. "auto-hide-rows": {
  47. name: "Min rows to hide",
  48. value: 10,
  49. processor: "int_range-1-",
  50. autoClose: false,
  51. title: "Minimum number of rows to hide"
  52. },
  53. "flat-layout": _boolDesc("Flat layout", "Use flat layout for script list and descriptions", false),
  54. "animation": _boolDesc("Animation", "Enable animation for toggling code blocks"),
  55. "search-syntax": _boolDesc("*Search syntax", "Enable partial search syntax for Greasy Fork search bar")
  56. };
  57. const config = GM_config(config_desc);
  58. // CSS
  59. const dynamicStyle = {
  60. "flat-layout": `
  61. .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; }
  62. 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; }
  63. .showing-all-languages .badge-js, .showing-all-languages .badge-css, .script-type { display: none; }
  64. ol.script-list > li > article > h2 > a.script-link { white-space: nowrap; }
  65. ol.script-list > li > article > h2 > span.script-description { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  66. ol.script-list > li > article > div.script-meta-block { width: 40%; column-gap: 0; }
  67. ol.script-list > li[data-script-type="library"] > article > h2 { width: 80%; }
  68. ol.script-list > li[data-script-type="library"] > article > div.script-meta-block { width: 20%; column-count: 1; }
  69. ol.script-list > li > article > div.script-meta-block > dl.inline-script-stats { margin: 0; }
  70. ol.script-list > li > article > div.script-meta-block > dl.inline-script-stats > dd { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  71. #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; }
  72. #additional-info { width: calc(100% - 2em - 2px); }`,
  73. "animation": `
  74. /* Toggle code animation */
  75. pre > code { transition: height 0.5s ease-in-out 0s; }
  76. /* Adapted from animate.css - https://animate.style/ */
  77. :root { --animate-duration: 1s; --animate-delay: 1s; --animate-repeat: 1; }
  78. .animate__animated { animation-duration: var(--animate-duration); animation-fill-mode: both; }
  79. .animate__animated.animate__fastest { animation-duration: calc(var(--animate-duration) / 3); }
  80. @keyframes tada {
  81. from { transform: scale3d(1, 1, 1); }
  82. 10%, 20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
  83. 30%, 50%, 70%, 90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
  84. 40%, 60%, 80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
  85. to { transform: scale3d(1, 1, 1); }
  86. }
  87. .animate__tada { animation-name: tada; }
  88. @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  89. .animate__fadeIn { animation-name: fadeIn; }
  90. @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
  91. .animate__fadeOut { -webkit-animation-name: fadeOut; animation-name: fadeOut; }`
  92. };
  93. // Functions
  94. const $ = document.querySelector.bind(document);
  95. const $$ = document.querySelectorAll.bind(document);
  96. const body = $("body");
  97. function sanitify(s) {
  98. // Remove emojis (such a headache)
  99. s = s.replaceAll(/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDEFF]|\uFE0F)/g, "");
  100. // Trim spaces and newlines
  101. s = s.trim();
  102. // Replace spaces
  103. s = s.replaceAll(" ", "-");
  104. s = s.replaceAll("%20", "-");
  105. // No more multiple "-"
  106. s = s.replaceAll(/-+/g, "-");
  107. return s;
  108. }
  109. function process(node) { // Add anchor and assign id to given node; Add to outline. Return true if node is actually processed.
  110. if (node.childElementCount > 1 || node.classList.length > 0) return false; // Ignore complex nodes
  111. const text = node.textContent;
  112. if (!node.id) { // If the node has no id
  113. node.id = sanitify(text); // Then assign id
  114. }
  115. // Add anchors
  116. const anchor = node.appendChild(document.createElement('a'));
  117. anchor.className = 'anchor';
  118. anchor.href = '#' + node.id;
  119. const link = outline.appendChild(document.createElement("li"))
  120. .appendChild(document.createElement("a"));
  121. link.href = "#" + node.id;
  122. link.text = text;
  123. return true;
  124. }
  125. async function animate(node, animation) {
  126. return new Promise((resolve, reject) => {
  127. node.classList.add("animate__animated", "animate__" + animation);
  128. if (node.getAnimations().length == 0) {
  129. node.classList.remove("animate__animated", "animate__" + animation);
  130. reject("No animation available");
  131. }
  132. node.addEventListener('animationend', e => {
  133. e.stopPropagation();
  134. node.classList.remove("animate__animated", "animate__" + animation);
  135. resolve("Animation ended");
  136. }, { once: true });
  137. });
  138. }
  139. async function transition(node, height) {
  140. return new Promise((resolve, reject) => {
  141. node.style.height = height;
  142. if (node.getAnimations().length == 0) {
  143. resolve("No transition available");
  144. }
  145. node.addEventListener('transitionend', e => {
  146. e.stopPropagation();
  147. resolve("Transition ended");
  148. }, { once: true });
  149. });
  150. }
  151. function copyCode() {
  152. const code = this.parentNode.nextElementSibling;
  153. const text = code.textContent;
  154. navigator.clipboard.writeText(text).then(() => {
  155. this.textContent = "Copied!";
  156. animate(this, "tada").then(() => {
  157. this.textContent = "Copy code";
  158. }, () => {
  159. window.setTimeout(() => {
  160. this.textContent = "Copy code";
  161. }, 1000);
  162. });
  163. });
  164. }
  165. function toggleCode() {
  166. const code = this.parentNode.nextElementSibling;
  167. if (code.style.height == "0px") {
  168. code.style.willChange = "height";
  169. transition(code, code.getAttribute("data-height")).then(() => {
  170. code.style.willChange = "";
  171. });
  172. animate(this, "fadeOut").then(() => {
  173. this.textContent = "Hide code";
  174. animate(this, "fadeIn");
  175. }, () => {
  176. this.textContent = "Hide code";
  177. });
  178. } else {
  179. code.style.willChange = "height";
  180. transition(code, "0px").then(() => {
  181. code.style.willChange = "";
  182. });
  183. animate(this, "fadeOut").then(() => {
  184. this.textContent = "Show code";
  185. animate(this, "fadeIn");
  186. }, () => {
  187. this.textContent = "Show code";
  188. });
  189. }
  190. }
  191. function create_toolbar() {
  192. const toolbar = document.createElement("div");
  193. const copy = toolbar.appendChild(document.createElement("a"));
  194. const toggle = toolbar.appendChild(document.createElement("a"));
  195. copy.textContent = "Copy code";
  196. copy.className = "code-operation";
  197. copy.title = "Copy code to clipboard";
  198. copy.addEventListener("click", copyCode);
  199. toggle.textContent = "Hide code";
  200. toggle.classList.add("code-operation", "animate__fastest");
  201. toggle.title = "Toggle code display";
  202. toggle.addEventListener("click", toggleCode);
  203. // Css
  204. toolbar.className = "code-toolbar";
  205. return toolbar;
  206. }
  207. function injectCSS(id, css) {
  208. const style = document.head.appendChild(document.createElement("style"));
  209. style.id = idPrefix + id;
  210. style.textContent = css;
  211. }
  212. function cssHelper(id, enable) {
  213. const current = document.getElementById(idPrefix + id);
  214. if (current) {
  215. current.disabled = !enable;
  216. } else if (enable) {
  217. injectCSS(id, dynamicStyle[id]);
  218. }
  219. }
  220. // Basic css
  221. injectCSS("basic", `
  222. html { scroll-behavior: smooth; }
  223. a.anchor::before { content: "#"; }
  224. a.anchor { opacity: 0; text-decoration: none; padding: 0px 0.5em; transition: all 0.25s ease-in-out; }
  225. h1:hover>a.anchor, h2:hover>a.anchor, h3:hover>a.anchor,
  226. h4:hover>a.anchor, h5:hover>a.anchor, h6:hover>a.anchor { opacity: 1; transition: all 0.25s ease-in-out; }
  227. 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; }
  228. div.code-toolbar { display: flex; gap: 1em; }
  229. a.code-operation { cursor: pointer; font-style: italic; }
  230. div.lum-lightbox { z-index: 2; }
  231. div#float-buttons { position: fixed; bottom: 1em; right: 1em; display: flex; flex-direction: column; user-select: none; z-index: 1; }
  232. aside.panel { display: none; }
  233. .dynamic-opacity { transition: opacity 0.2s ease-in-out; opacity: 0.2; }
  234. .dynamic-opacity:hover { opacity: 0.8; }
  235. 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; }
  236. input[type=file]:hover { border-color: black; background: rgba(169, 169, 169, 0.6); }
  237. table { border: 1px solid #8d8d8d; border-collapse: collapse; width: auto; }
  238. table td, table th { padding: 0.5em 0.75em; vertical-align: middle; border: 1px solid #8d8d8d; }
  239. @media (any-hover: none) { .dynamic-opacity { opacity: 0.8; } .dynamic-opacity:hover { opacity: 0.8; } }
  240. @media screen and (min-width: 767px) {
  241. aside.panel { display: contents; line-height: 1.5; }
  242. 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; }
  243. ul.outline > li { overflow: hidden; text-overflow: ellipsis; }
  244. ul.outline > li > a { color: gray; white-space: nowrap; text-decoration: none; }
  245. }
  246. pre > code { overflow: hidden; display: block; }
  247. ul { padding-left: 1.5em; }`);
  248. // Aside panel & Anchors
  249. let outline;
  250. const is_script = /^\/[^\/]+\/scripts/;
  251. const is_specific_script = /^\/[^\/]+\/scripts\/\d+/;
  252. const is_disccussion = /^\/[^\/]+\/discussions/;
  253. const path = window.location.pathname;
  254. if ((!is_script.test(path) && !is_disccussion.test(path)) || is_specific_script.test(path)) {
  255. const panel = body.insertBefore(document.createElement("aside"), $("body > div.width-constraint"));
  256. panel.className = "panel";
  257. const reference_node = $("body > div.width-constraint > section");
  258. outline = panel.appendChild(document.createElement("ul"));
  259. outline.classList.add("outline");
  260. outline.classList.add("dynamic-opacity");
  261. outline.style.top = reference_node ? getComputedStyle(reference_node).marginTop : "1em";
  262. outline.style.marginTop = outline.style.top;
  263. let flag = false;
  264. $$("body > div.width-constraint h1, h2, h3, h4, h5, h6").forEach((node) => {
  265. flag = process(node) || flag; // Not `flag || process(node)`!
  266. });
  267. if (!flag) {
  268. panel.remove();
  269. }
  270. }
  271. // Navigate to hash
  272. const hash = window.location.hash.slice(1);
  273. if (hash) {
  274. const ele = document.getElementById(decodeURIComponent(hash));
  275. if (ele) {
  276. ele.scrollIntoView();
  277. }
  278. }
  279. // Buttons
  280. const buttons = body.appendChild(document.createElement("div"));
  281. buttons.id = "float-buttons";
  282. const to_top = buttons.appendChild(document.createElement("a"));
  283. to_top.classList.add("button");
  284. to_top.classList.add("dynamic-opacity");
  285. to_top.href = "#top";
  286. to_top.text = "↑";
  287. // Double click to get to top
  288. body.addEventListener("dblclick", (e) => {
  289. if (e.target === body) {
  290. to_top.click();
  291. }
  292. });
  293. // Fix current tab link
  294. const tab = $("ul#script-links > li.current");
  295. if (tab) {
  296. const link = tab.appendChild(document.createElement("a"));
  297. link.href = window.location.pathname;
  298. link.appendChild(tab.firstChild);
  299. }
  300. const parts = window.location.pathname.split("/");
  301. if (parts.length <= 2 || (parts.length == 3 && parts[2] === '')) {
  302. const banner = $("header#main-header div#site-name");
  303. const img = banner.querySelector("img");
  304. const text = banner.querySelector("#site-name-text > h1");
  305. const link1 = document.createElement("a");
  306. link1.href = window.location.pathname;
  307. img.parentNode.replaceChild(link1, img);
  308. link1.appendChild(img);
  309. const link2 = document.createElement("a");
  310. link2.href = window.location.pathname;
  311. link2.textContent = text.textContent;
  312. text.textContent = "";
  313. text.appendChild(link2);
  314. }
  315. // Toolbar for code blocks
  316. const code_blocks = document.getElementsByTagName("pre");
  317. const auto_hide = config["auto-hide-code"];
  318. const auto_hide_rows = config["auto-hide-rows"];
  319. for (const code_block of code_blocks) {
  320. if (code_block.firstChild.tagName === "CODE") {
  321. const height = getComputedStyle(code_block.firstChild).getPropertyValue("height");
  322. code_block.firstChild.style.height = height;
  323. code_block.firstChild.setAttribute("data-height", height);
  324. code_block.insertAdjacentElement("afterbegin", create_toolbar());
  325. }
  326. }
  327. // Auto hide code blocks
  328. function autoHide() {
  329. if (!auto_hide) {
  330. for (const code_block of code_blocks) {
  331. const toggle = code_block.firstChild.lastChild;
  332. if (toggle.textContent === "Show code") {
  333. toggle.click(); // Click the toggle button
  334. }
  335. }
  336. } else {
  337. for (const code_block of code_blocks) {
  338. const m = code_block.lastChild.textContent.match(/\n/g);
  339. const rows = m ? m.length : 0;
  340. const toggle = code_block.firstChild.lastChild;
  341. const hidden = toggle.textContent === "Show code";
  342. if (rows >= auto_hide_rows && !hidden || rows < auto_hide_rows && hidden) {
  343. code_block.firstChild.lastChild.click(); // Click the toggle button
  344. }
  345. }
  346. }
  347. }
  348. document.addEventListener("readystatechange", (e) => {
  349. if (e.target.readyState === "complete") {
  350. autoHide();
  351. }
  352. }, { once: true });
  353. // Initialize css
  354. for (const prop in dynamicStyle) {
  355. cssHelper(prop, config[prop]);
  356. }
  357. // Dynamically respond to config changes
  358. const callbacks = {
  359. "auto-hide-code": (after) => {
  360. auto_hide = after;
  361. autoHide();
  362. },
  363. "auto-hide-rows": (after) => {
  364. auto_hide_rows = after;
  365. autoHide();
  366. },
  367. "flat-layout": (after) => {
  368. const meta_orig = $("#script-info > #script-content > .script-meta-block");
  369. const meta_mod = $("#script-info > .script-meta-block");
  370. if (after && meta_orig) {
  371. const links = $("#script-info > #script-links");
  372. links.after(meta_orig);
  373. } else if (!after && meta_mod) {
  374. const additional = $("#script-info > #script-content > #additional-info");
  375. additional.before(meta_mod);
  376. }
  377. },
  378. };
  379. callbacks["flat-layout"](config["flat-layout"]);
  380. window.addEventListener(GM_config_event, e => {
  381. if (e.detail.type === "set") {
  382. const callback = callbacks[e.detail.prop];
  383. if (callback && (e.detail.before !== e.detail.after)) {
  384. callback(e.detail.after);
  385. }
  386. if (e.detail.prop in dynamicStyle) {
  387. cssHelper(e.detail.prop, e.detail.after);
  388. }
  389. }
  390. });
  391. // Search syntax
  392. if (config["search-syntax"]) {
  393. const search = $("input[type=search][name=q]");
  394. const submit = $("input[type=submit]");
  395. if (!search || !submit) return; // No search bar
  396. const form = search.parentElement;
  397. // site:site-name
  398. function parseSite(s) {
  399. const m = s.match(/\bsite:(\S*)/);
  400. if (m) {
  401. return m[1];
  402. } else {
  403. return undefined;
  404. }
  405. }
  406. form.addEventListener("submit", (e) => {
  407. const site = parseSite(search.value);
  408. if (site) {
  409. search.value = search.value.replace(/\s*\bsite:\S*/, "");
  410. form.action = `/scripts/by-site/${site}?site=${site}`;
  411. }
  412. });
  413. }
  414. })();