Greasy Fork Enhance

Enhance your experience at Greasyfork.

目前為 2023-08-06 提交的版本,檢視 最新版本

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