Greasy Fork 增强

增进 Greasyfork 浏览体验。

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

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