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.6
  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. resolve("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. });
  243. }
  244. function toggleCode() {
  245. let code = this.parentNode.nextElementSibling;
  246. if (code.style.height == "0px") {
  247. code.style.willChange = "height";
  248. transition(code, code.getAttribute("data-height")).then(() => {
  249. code.style.willChange = "";
  250. });
  251. animate(this, "fadeOut").then(() => {
  252. this.textContent = "Hide code";
  253. animate(this, "fadeIn");
  254. });
  255. } else {
  256. code.style.willChange = "height";
  257. transition(code, "0px").then(() => {
  258. code.style.willChange = "";
  259. });
  260. animate(this, "fadeOut").then(() => {
  261. this.textContent = "Show code";
  262. animate(this, "fadeIn");
  263. });
  264. }
  265. }
  266. function create_toolbar() {
  267. let toolbar = document.createElement("div");
  268. let copy = document.createElement("a");
  269. let toggle = document.createElement("a");
  270. toolbar.appendChild(copy);
  271. toolbar.appendChild(toggle);
  272. copy.textContent = "Copy code";
  273. copy.className = "code-operation";
  274. copy.title = "Copy code to clipboard";
  275. copy.addEventListener("click", copyCode);
  276. toggle.textContent = "Hide code";
  277. toggle.classList.add("code-operation", "animate__fastest");
  278. toggle.title = "Toggle code display";
  279. toggle.addEventListener("click", toggleCode);
  280. // Css
  281. toolbar.className = "code-toolbar";
  282. return toolbar;
  283. }
  284. function injectCSS(id, css) {
  285. let style = document.createElement("style");
  286. style.id = idPrefix + id;
  287. style.textContent = css;
  288. document.head.appendChild(style);
  289. }
  290. function cssHelper(id, enable) {
  291. let current = document.getElementById(idPrefix + id);
  292. if (current) {
  293. current.disabled = !enable;
  294. } else if (enable) {
  295. injectCSS(id, dynamicStyle[id]);
  296. }
  297. }
  298. // Basic css
  299. injectCSS("basic", `
  300. html {
  301. scroll-behavior: smooth;
  302. }
  303. a.anchor::before {
  304. content: "#";
  305. }
  306. a.anchor {
  307. opacity: 0;
  308. text-decoration: none;
  309. padding: 0px 0.5em;
  310. transition: all 0.25s ease-in-out;
  311. }
  312. h1:hover>a.anchor, h2:hover>a.anchor, h3:hover>a.anchor,
  313. h4:hover>a.anchor, h5:hover>a.anchor, h6:hover>a.anchor {
  314. opacity: 1;
  315. transition: all 0.25s ease-in-out;
  316. }
  317. a.button {
  318. margin: 0.5em 0 0 0;
  319. display: flex;
  320. align-items: center;
  321. justify-content: center;
  322. text-decoration: none;
  323. color: black;
  324. background-color: #a42121ab;
  325. border-radius: 50%;
  326. width: 2em;
  327. height: 2em;
  328. font-size: 1.8em;
  329. font-weight: bold;
  330. }
  331. div.code-toolbar {
  332. display: flex;
  333. gap: 1em;
  334. }
  335. a.code-operation {
  336. cursor: pointer;
  337. font-style: italic;
  338. }
  339. div.lum-lightbox {
  340. z-index: 2;
  341. }
  342. div#float-buttons {
  343. position: fixed;
  344. bottom: 1em;
  345. right: 1em;
  346. display: flex;
  347. flex-direction: column;
  348. user-select: none;
  349. z-index: 1;
  350. }
  351. aside.panel {
  352. display: none;
  353. }
  354. .dynamic-opacity {
  355. transition: opacity 0.2s ease-in-out;
  356. opacity: 0.2;
  357. }
  358. .dynamic-opacity:hover {
  359. opacity: 0.8;
  360. }
  361. input[type=file] {
  362. border-style: dashed;
  363. border-radius: 0.5em;
  364. padding: 0.5em;
  365. background: rgba(169, 169, 169, 0.4);
  366. }
  367. table {
  368. border: 1px solid #8d8d8d;
  369. border-collapse: collapse;
  370. width: auto;
  371. }
  372. table td, table th {
  373. padding: 0.5em 0.75em;
  374. vertical-align: middle;
  375. border: 1px solid #8d8d8d;
  376. }
  377. @media (any-hover: none) {
  378. .dynamic-opacity {
  379. opacity: 0.8;
  380. }
  381. .dynamic-opacity:hover {
  382. opacity: 0.8;
  383. }
  384. }
  385. @media screen and (min-width: 767px) {
  386. aside.panel {
  387. display: contents;
  388. line-height: 1.5;
  389. }
  390. ul.outline {
  391. position: sticky;
  392. float: right;
  393. padding: 0 0 0 0.5em;
  394. margin: 0 0.5em;
  395. max-height: 80vh;
  396. border: 1px solid #BBBBBB;
  397. border-left: 2px solid #F2E5E5;
  398. box-shadow: 0 0 5px #ddd;
  399. background: linear-gradient(to right, #fcf1f1, #FFF 1em);
  400. list-style: none;
  401. width: 10.5%;
  402. color: gray;
  403. border-radius: 5px;
  404. overflow-y: scroll;
  405. z-index: 1;
  406. }
  407. ul.outline > li {
  408. overflow: hidden;
  409. text-overflow: ellipsis;
  410. }
  411. ul.outline > li > a {
  412. color: gray;
  413. white-space: nowrap;
  414. text-decoration: none;
  415. }
  416. }
  417. pre > code {
  418. overflow: hidden;
  419. display: block;
  420. }`);
  421. // Aside panel & Anchors
  422. let outline;
  423. let is_script = /^\/[^\/]+\/scripts/;
  424. let is_specific_script = /^\/[^\/]+\/scripts\/\d+/;
  425. let is_disccussion = /^\/[^\/]+\/discussions/;
  426. let path = window.location.pathname;
  427. if ((!is_script.test(path) && !is_disccussion.test(path)) || is_specific_script.test(path)) {
  428. let panel = document.createElement("aside");
  429. panel.className = "panel";
  430. body.insertBefore(panel, document.querySelector("body > div.width-constraint"));
  431. let reference_node = document.querySelector("body > div.width-constraint > section");
  432. outline = document.createElement("ul");
  433. outline.classList.add("outline");
  434. outline.classList.add("dynamic-opacity");
  435. outline.style.top = reference_node ? getComputedStyle(reference_node).marginTop : "1em";
  436. outline.style.marginTop = outline.style.top;
  437. panel.appendChild(outline);
  438. let flag = false;
  439. document.querySelectorAll("body > div.width-constraint h1, h2, h3, h4, h5, h6").forEach((node) => {
  440. flag = process(node) || flag; // Not `flag || process(node)`!
  441. });
  442. if (!flag) {
  443. panel.remove();
  444. }
  445. }
  446. // Navigate to hash
  447. let hash = window.location.hash.slice(1);
  448. if (hash) {
  449. let ele = document.getElementById(decodeURIComponent(hash));
  450. if (ele) {
  451. ele.scrollIntoView();
  452. }
  453. }
  454. // Buttons
  455. let buttons = document.createElement("div");
  456. buttons.id = "float-buttons";
  457. let to_top = document.createElement("a");
  458. to_top.classList.add("button");
  459. to_top.classList.add("dynamic-opacity");
  460. to_top.href = "#top";
  461. to_top.text = "↑";
  462. buttons.appendChild(to_top);
  463. body.appendChild(buttons);
  464. // Double click to get to top
  465. body.addEventListener("dblclick", (e) => {
  466. if (e.target === body) {
  467. to_top.click();
  468. }
  469. });
  470. // Fix current tab link
  471. let tab = document.querySelector("ul#script-links > li.current");
  472. if (tab) {
  473. let link = document.createElement("a");
  474. link.href = window.location.pathname;
  475. let orig_child = tab.firstChild;
  476. link.appendChild(orig_child);
  477. tab.appendChild(link);
  478. }
  479. let parts = window.location.pathname.split("/");
  480. if (parts.length <= 2 || (parts.length == 3 && parts[2] === '')) {
  481. let banner = document.querySelector("header#main-header div#site-name");
  482. let img = banner.querySelector("img");
  483. let text = banner.querySelector("#site-name-text > h1");
  484. let link1 = document.createElement("a");
  485. link1.href = window.location.pathname;
  486. img.parentNode.replaceChild(link1, img);
  487. link1.appendChild(img);
  488. let link2 = document.createElement("a");
  489. link2.href = window.location.pathname;
  490. link2.textContent = text.textContent;
  491. text.textContent = "";
  492. text.appendChild(link2);
  493. }
  494. // Toolbar for code blocks
  495. let code_blocks = document.getElementsByTagName("pre");
  496. let auto_hide = config["auto-hide-code"];
  497. let auto_hide_rows = config["auto-hide-rows"];
  498. for (let code_block of code_blocks) {
  499. if (code_block.firstChild.tagName === "CODE") {
  500. let height = getComputedStyle(code_block.firstChild).getPropertyValue("height");
  501. code_block.firstChild.style.height = height;
  502. code_block.firstChild.setAttribute("data-height", height);
  503. code_block.insertAdjacentElement("afterbegin", create_toolbar());
  504. }
  505. }
  506. // Auto hide code blocks
  507. function autoHide() {
  508. if (!auto_hide) {
  509. for (let code_block of code_blocks) {
  510. let toggle = code_block.firstChild.lastChild;
  511. if (toggle.textContent === "Show code") {
  512. toggle.click(); // Click the toggle button
  513. }
  514. }
  515. } else {
  516. for (let code_block of code_blocks) {
  517. let m = code_block.lastChild.textContent.match(/\n/g);
  518. let rows = m ? m.length : 0;
  519. let toggle = code_block.firstChild.lastChild;
  520. let hidden = toggle.textContent === "Show code";
  521. if (rows >= auto_hide_rows && !hidden || rows < auto_hide_rows && hidden) {
  522. code_block.firstChild.lastChild.click(); // Click the toggle button
  523. }
  524. }
  525. }
  526. }
  527. document.addEventListener("readystatechange", (e) => {
  528. if (e.target.readyState === "complete") {
  529. autoHide();
  530. }
  531. }, {once: true});
  532. // Initialize css
  533. for (let prop in dynamicStyle) {
  534. cssHelper(prop, config[prop]);
  535. }
  536. // Dynamically respond to config changes
  537. let callbacks = {
  538. "auto-hide-code": (after) => {
  539. auto_hide = after;
  540. autoHide();
  541. },
  542. "auto-hide-rows": (after) => {
  543. auto_hide_rows = after;
  544. autoHide();
  545. }
  546. };
  547. window.addEventListener(GM_config_event, e => {
  548. if (e.detail.type === "set") {
  549. let callback = callbacks[e.detail.prop];
  550. if (callback && (e.detail.before !== e.detail.after)) {
  551. callback(e.detail.after);
  552. } else if (e.detail.prop in dynamicStyle) {
  553. cssHelper(e.detail.prop, e.detail.after);
  554. }
  555. }
  556. });
  557. })();