Greasy Fork 增强

增进 Greasyfork 浏览体验。

当前为 2023-08-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Greasy Fork Enhance
  3. // @name:zh-CN Greasy Fork 增强
  4. // @namespace http://tampermonkey.net/
  5. // @version 0.5.7
  6. // @description Enhance your experience at Greasyfork.
  7. // @description:zh-CN 增进 Greasyfork 浏览体验。
  8. // @author PRO
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_deleteValue
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_unregisterMenuCommand
  14. // @match https://greasyfork.org/*
  15. // @require https://greasyfork.org/scripts/470224-tampermonkey-config/code/Tampermonkey%20Config.js?version=1230660
  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. let no_run = [".json", ".js"];
  24. let is_run = true;
  25. let 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. let config_desc = {
  34. "auto-hide-code": {
  35. name: "Auto hide code",
  36. value: true,
  37. input: "current",
  38. processor: "not",
  39. formatter: "boolean"
  40. },
  41. "auto-hide-rows": {
  42. name: "Min rows to hide",
  43. value: 10,
  44. processor: "int_range-1-"
  45. },
  46. "flat-layout": {
  47. name: "Flat layout",
  48. value: false,
  49. input: "current",
  50. processor: "not",
  51. formatter: "boolean"
  52. },
  53. "animation": {
  54. name: "Animation",
  55. value: true,
  56. input: "current",
  57. processor: "not",
  58. formatter: "boolean"
  59. }
  60. };
  61. let config = GM_config(config_desc);
  62. // CSS
  63. const dynamicStyle = {
  64. "flat-layout": `
  65. .script-list li:not(.ad-entry) {
  66. padding-right: 0;
  67. }
  68. ol.script-list > li > article {
  69. display: flex;
  70. flex-direction: row;
  71. justify-content: space-between;
  72. align-items: center;
  73. }
  74. ol.script-list > li > article > h2 {
  75. width: 60%;
  76. overflow: hidden;
  77. text-overflow: ellipsis;
  78. margin-right: 0.5em;
  79. padding-right: 0.5em;
  80. border-right: 1px solid #DDDDDD;
  81. }
  82. .showing-all-languages .badge-js, .showing-all-languages .badge-css, .script-type {
  83. display: none;
  84. }
  85. ol.script-list > li > article > h2 > a.script-link {
  86. white-space: nowrap;
  87. }
  88. ol.script-list > li > article > h2 > span.script-description {
  89. display: block;
  90. white-space: nowrap;
  91. overflow: hidden;
  92. text-overflow: ellipsis;
  93. }
  94. ol.script-list > li > article > div.script-meta-block {
  95. width: 40%;
  96. column-gap: 0;
  97. }
  98. ol.script-list > li[data-script-type="library"] > article > h2 {
  99. width: 80%;
  100. }
  101. ol.script-list > li[data-script-type="library"] > article > div.script-meta-block {
  102. width: 20%;
  103. column-count: 1;
  104. }
  105. ol.script-list > li > article > div.script-meta-block > dl.inline-script-stats {
  106. margin: 0;
  107. }
  108. ol.script-list > li > article > div.script-meta-block > dl.inline-script-stats > dd {
  109. white-space: nowrap;
  110. overflow: hidden;
  111. text-overflow: ellipsis;
  112. }
  113. `,
  114. "animation": `
  115. /* Toggle code animation */
  116. pre > code {
  117. /*will-change: height;*/
  118. transition: height 0.5s ease-in-out 0s;
  119. }
  120. /* Adapted from animate.css - https://animate.style/ */
  121. :root {
  122. --animate-duration: 1s;
  123. --animate-delay: 1s;
  124. --animate-repeat: 1;
  125. }
  126. .animate__animated {
  127. animation-duration: var(--animate-duration);
  128. animation-fill-mode: both;
  129. }
  130. .animate__animated.animate__fastest {
  131. animation-duration: calc(var(--animate-duration) / 3);
  132. }
  133. @keyframes tada {
  134. from {
  135. transform: scale3d(1, 1, 1);
  136. }
  137. 10%, 20% {
  138. transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
  139. }
  140. 30%, 50%, 70%, 90% {
  141. transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
  142. }
  143. 40%, 60%, 80% {
  144. transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
  145. }
  146. to {
  147. transform: scale3d(1, 1, 1);
  148. }
  149. }
  150. .animate__tada {
  151. animation-name: tada;
  152. }
  153. @keyframes fadeIn {
  154. from {
  155. opacity: 0;
  156. }
  157. to {
  158. opacity: 1;
  159. }
  160. }
  161. .animate__fadeIn {
  162. animation-name: fadeIn;
  163. }
  164. @keyframes fadeOut {
  165. from {
  166. opacity: 1;
  167. }
  168. to {
  169. opacity: 0;
  170. }
  171. }
  172. .animate__fadeOut {
  173. -webkit-animation-name: fadeOut;
  174. animation-name: fadeOut;
  175. }`
  176. };
  177. // Functions
  178. let body = document.querySelector("body");
  179. function sanitify(s) {
  180. // Remove emojis (such a headache)
  181. s = s.replaceAll(/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDEFF]|\uFE0F)/g, "");
  182. // Trim spaces and newlines
  183. s = s.trim();
  184. // Replace spaces
  185. s = s.replaceAll(" ", "-");
  186. s = s.replaceAll("%20", "-");
  187. // No more multiple "-"
  188. s = s.replaceAll(/-+/g, "-");
  189. return s;
  190. }
  191. function process(node) { // Add anchor and assign id to given node; Add to outline. Return true if node is actually processed.
  192. if (node.childElementCount > 1 || node.classList.length > 0) return false; // Ignore complex nodes
  193. let text = node.textContent;
  194. node.id = sanitify(text); // Assign id
  195. // Add anchors
  196. let node_ = document.createElement('a');
  197. node_.className = 'anchor';
  198. node_.href = '#' + node.id;
  199. node.appendChild(node_);
  200. let list_item = document.createElement("li");
  201. outline.appendChild(list_item);
  202. let link = document.createElement("a");
  203. link.href = "#" + node.id;
  204. link.text = text;
  205. list_item.appendChild(link);
  206. return true;
  207. }
  208. async function animate(node, animation) {
  209. return new Promise((resolve, reject) => {
  210. node.classList.add("animate__animated", "animate__" + animation);
  211. if (node.getAnimations().length == 0) {
  212. node.classList.remove("animate__animated", "animate__" + animation);
  213. reject("No animation available");
  214. }
  215. node.addEventListener('animationend', e => {
  216. e.stopPropagation();
  217. node.classList.remove("animate__animated", "animate__" + animation);
  218. resolve("Animation ended");
  219. }, { once: true });
  220. });
  221. }
  222. async function transition(node, height) {
  223. return new Promise((resolve, reject) => {
  224. node.style.height = height;
  225. if (node.getAnimations().length == 0) {
  226. resolve("No transition available");
  227. }
  228. node.addEventListener('transitionend', e => {
  229. e.stopPropagation();
  230. resolve("Transition ended");
  231. }, { once: true });
  232. });
  233. }
  234. function copyCode() {
  235. let code = this.parentNode.nextElementSibling;
  236. let text = code.textContent;
  237. navigator.clipboard.writeText(text).then(() => {
  238. this.textContent = "Copied!";
  239. animate(this, "tada").then(() => {
  240. this.textContent = "Copy code";
  241. }, () => {
  242. window.setTimeout(() => {
  243. this.textContent = "Copy code";
  244. }, 1000);
  245. });
  246. });
  247. }
  248. function toggleCode() {
  249. let code = this.parentNode.nextElementSibling;
  250. if (code.style.height == "0px") {
  251. code.style.willChange = "height";
  252. transition(code, code.getAttribute("data-height")).then(() => {
  253. code.style.willChange = "";
  254. });
  255. animate(this, "fadeOut").then(() => {
  256. this.textContent = "Hide code";
  257. animate(this, "fadeIn");
  258. }, () => {
  259. this.textContent = "Hide code";
  260. });
  261. } else {
  262. code.style.willChange = "height";
  263. transition(code, "0px").then(() => {
  264. code.style.willChange = "";
  265. });
  266. animate(this, "fadeOut").then(() => {
  267. this.textContent = "Show code";
  268. animate(this, "fadeIn");
  269. }, () => {
  270. this.textContent = "Show code";
  271. });
  272. }
  273. }
  274. function create_toolbar() {
  275. let toolbar = document.createElement("div");
  276. let copy = document.createElement("a");
  277. let toggle = document.createElement("a");
  278. toolbar.appendChild(copy);
  279. toolbar.appendChild(toggle);
  280. copy.textContent = "Copy code";
  281. copy.className = "code-operation";
  282. copy.title = "Copy code to clipboard";
  283. copy.addEventListener("click", copyCode);
  284. toggle.textContent = "Hide code";
  285. toggle.classList.add("code-operation", "animate__fastest");
  286. toggle.title = "Toggle code display";
  287. toggle.addEventListener("click", toggleCode);
  288. // Css
  289. toolbar.className = "code-toolbar";
  290. return toolbar;
  291. }
  292. function injectCSS(id, css) {
  293. let style = document.createElement("style");
  294. style.id = idPrefix + id;
  295. style.textContent = css;
  296. document.head.appendChild(style);
  297. }
  298. function cssHelper(id, enable) {
  299. let current = document.getElementById(idPrefix + id);
  300. if (current) {
  301. current.disabled = !enable;
  302. } else if (enable) {
  303. injectCSS(id, dynamicStyle[id]);
  304. }
  305. }
  306. // Basic css
  307. injectCSS("basic", `
  308. html {
  309. scroll-behavior: smooth;
  310. }
  311. a.anchor::before {
  312. content: "#";
  313. }
  314. a.anchor {
  315. opacity: 0;
  316. text-decoration: none;
  317. padding: 0px 0.5em;
  318. transition: all 0.25s ease-in-out;
  319. }
  320. h1:hover>a.anchor, h2:hover>a.anchor, h3:hover>a.anchor,
  321. h4:hover>a.anchor, h5:hover>a.anchor, h6:hover>a.anchor {
  322. opacity: 1;
  323. transition: all 0.25s ease-in-out;
  324. }
  325. a.button {
  326. margin: 0.5em 0 0 0;
  327. display: flex;
  328. align-items: center;
  329. justify-content: center;
  330. text-decoration: none;
  331. color: black;
  332. background-color: #a42121ab;
  333. border-radius: 50%;
  334. width: 2em;
  335. height: 2em;
  336. font-size: 1.8em;
  337. font-weight: bold;
  338. }
  339. div.code-toolbar {
  340. display: flex;
  341. gap: 1em;
  342. }
  343. a.code-operation {
  344. cursor: pointer;
  345. font-style: italic;
  346. }
  347. div.lum-lightbox {
  348. z-index: 2;
  349. }
  350. div#float-buttons {
  351. position: fixed;
  352. bottom: 1em;
  353. right: 1em;
  354. display: flex;
  355. flex-direction: column;
  356. user-select: none;
  357. z-index: 1;
  358. }
  359. aside.panel {
  360. display: none;
  361. }
  362. .dynamic-opacity {
  363. transition: opacity 0.2s ease-in-out;
  364. opacity: 0.2;
  365. }
  366. .dynamic-opacity:hover {
  367. opacity: 0.8;
  368. }
  369. input[type=file] {
  370. border-style: dashed;
  371. border-radius: 0.5em;
  372. padding: 0.5em;
  373. background: rgba(169, 169, 169, 0.4);
  374. }
  375. table {
  376. border: 1px solid #8d8d8d;
  377. border-collapse: collapse;
  378. width: auto;
  379. }
  380. table td, table th {
  381. padding: 0.5em 0.75em;
  382. vertical-align: middle;
  383. border: 1px solid #8d8d8d;
  384. }
  385. @media (any-hover: none) {
  386. .dynamic-opacity {
  387. opacity: 0.8;
  388. }
  389. .dynamic-opacity:hover {
  390. opacity: 0.8;
  391. }
  392. }
  393. @media screen and (min-width: 767px) {
  394. aside.panel {
  395. display: contents;
  396. line-height: 1.5;
  397. }
  398. ul.outline {
  399. position: sticky;
  400. float: right;
  401. padding: 0 0 0 0.5em;
  402. margin: 0 0.5em;
  403. max-height: 80vh;
  404. border: 1px solid #BBBBBB;
  405. border-left: 2px solid #F2E5E5;
  406. box-shadow: 0 0 5px #ddd;
  407. background: linear-gradient(to right, #fcf1f1, #FFF 1em);
  408. list-style: none;
  409. width: 10.5%;
  410. color: gray;
  411. border-radius: 5px;
  412. overflow-y: scroll;
  413. z-index: 1;
  414. }
  415. ul.outline > li {
  416. overflow: hidden;
  417. text-overflow: ellipsis;
  418. }
  419. ul.outline > li > a {
  420. color: gray;
  421. white-space: nowrap;
  422. text-decoration: none;
  423. }
  424. }
  425. pre > code {
  426. overflow: hidden;
  427. display: block;
  428. }`);
  429. // Aside panel & Anchors
  430. let outline;
  431. let is_script = /^\/[^\/]+\/scripts/;
  432. let is_specific_script = /^\/[^\/]+\/scripts\/\d+/;
  433. let is_disccussion = /^\/[^\/]+\/discussions/;
  434. let path = window.location.pathname;
  435. if ((!is_script.test(path) && !is_disccussion.test(path)) || is_specific_script.test(path)) {
  436. let panel = document.createElement("aside");
  437. panel.className = "panel";
  438. body.insertBefore(panel, document.querySelector("body > div.width-constraint"));
  439. let reference_node = document.querySelector("body > div.width-constraint > section");
  440. outline = document.createElement("ul");
  441. outline.classList.add("outline");
  442. outline.classList.add("dynamic-opacity");
  443. outline.style.top = reference_node ? getComputedStyle(reference_node).marginTop : "1em";
  444. outline.style.marginTop = outline.style.top;
  445. panel.appendChild(outline);
  446. let flag = false;
  447. document.querySelectorAll("body > div.width-constraint h1, h2, h3, h4, h5, h6").forEach((node) => {
  448. flag = process(node) || flag; // Not `flag || process(node)`!
  449. });
  450. if (!flag) {
  451. panel.remove();
  452. }
  453. }
  454. // Navigate to hash
  455. let hash = window.location.hash.slice(1);
  456. if (hash) {
  457. let ele = document.getElementById(decodeURIComponent(hash));
  458. if (ele) {
  459. ele.scrollIntoView();
  460. }
  461. }
  462. // Buttons
  463. let buttons = document.createElement("div");
  464. buttons.id = "float-buttons";
  465. let to_top = document.createElement("a");
  466. to_top.classList.add("button");
  467. to_top.classList.add("dynamic-opacity");
  468. to_top.href = "#top";
  469. to_top.text = "↑";
  470. buttons.appendChild(to_top);
  471. body.appendChild(buttons);
  472. // Double click to get to top
  473. body.addEventListener("dblclick", (e) => {
  474. if (e.target === body) {
  475. to_top.click();
  476. }
  477. });
  478. // Fix current tab link
  479. let tab = document.querySelector("ul#script-links > li.current");
  480. if (tab) {
  481. let link = document.createElement("a");
  482. link.href = window.location.pathname;
  483. let orig_child = tab.firstChild;
  484. link.appendChild(orig_child);
  485. tab.appendChild(link);
  486. }
  487. let parts = window.location.pathname.split("/");
  488. if (parts.length <= 2 || (parts.length == 3 && parts[2] === '')) {
  489. let banner = document.querySelector("header#main-header div#site-name");
  490. let img = banner.querySelector("img");
  491. let text = banner.querySelector("#site-name-text > h1");
  492. let link1 = document.createElement("a");
  493. link1.href = window.location.pathname;
  494. img.parentNode.replaceChild(link1, img);
  495. link1.appendChild(img);
  496. let link2 = document.createElement("a");
  497. link2.href = window.location.pathname;
  498. link2.textContent = text.textContent;
  499. text.textContent = "";
  500. text.appendChild(link2);
  501. }
  502. // Toolbar for code blocks
  503. let code_blocks = document.getElementsByTagName("pre");
  504. let auto_hide = config["auto-hide-code"];
  505. let auto_hide_rows = config["auto-hide-rows"];
  506. for (let code_block of code_blocks) {
  507. if (code_block.firstChild.tagName === "CODE") {
  508. let height = getComputedStyle(code_block.firstChild).getPropertyValue("height");
  509. code_block.firstChild.style.height = height;
  510. code_block.firstChild.setAttribute("data-height", height);
  511. code_block.insertAdjacentElement("afterbegin", create_toolbar());
  512. }
  513. }
  514. // Auto hide code blocks
  515. function autoHide() {
  516. if (!auto_hide) {
  517. for (let code_block of code_blocks) {
  518. let toggle = code_block.firstChild.lastChild;
  519. if (toggle.textContent === "Show code") {
  520. toggle.click(); // Click the toggle button
  521. }
  522. }
  523. } else {
  524. for (let code_block of code_blocks) {
  525. let m = code_block.lastChild.textContent.match(/\n/g);
  526. let rows = m ? m.length : 0;
  527. let toggle = code_block.firstChild.lastChild;
  528. let hidden = toggle.textContent === "Show code";
  529. if (rows >= auto_hide_rows && !hidden || rows < auto_hide_rows && hidden) {
  530. code_block.firstChild.lastChild.click(); // Click the toggle button
  531. }
  532. }
  533. }
  534. }
  535. document.addEventListener("readystatechange", (e) => {
  536. if (e.target.readyState === "complete") {
  537. autoHide();
  538. }
  539. }, {once: true});
  540. // Initialize css
  541. for (let prop in dynamicStyle) {
  542. cssHelper(prop, config[prop]);
  543. }
  544. // Dynamically respond to config changes
  545. let callbacks = {
  546. "auto-hide-code": (after) => {
  547. auto_hide = after;
  548. autoHide();
  549. },
  550. "auto-hide-rows": (after) => {
  551. auto_hide_rows = after;
  552. autoHide();
  553. }
  554. };
  555. window.addEventListener(GM_config_event, e => {
  556. if (e.detail.type === "set") {
  557. let callback = callbacks[e.detail.prop];
  558. if (callback && (e.detail.before !== e.detail.after)) {
  559. callback(e.detail.after);
  560. } else if (e.detail.prop in dynamicStyle) {
  561. cssHelper(e.detail.prop, e.detail.after);
  562. }
  563. }
  564. });
  565. })();