// ==UserScript==
// @name Greasy Fork Enhance
// @name:zh-CN Greasy Fork 增强
// @namespace http://tampermonkey.net/
// @version 0.5.7
// @description Enhance your experience at Greasyfork.
// @description:zh-CN 增进 Greasyfork 浏览体验。
// @author PRO
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @match https://greasyfork.org/*
// @require https://greasyfork.org/scripts/470224-tampermonkey-config/code/Tampermonkey%20Config.js?version=1230660
// @icon https://greasyfork.org/vite/assets/blacklogo16-bc64b9f7.png
// @license gpl-3.0
// ==/UserScript==
(function() {
'use strict';
// Judge if the script should run
let no_run = [".json", ".js"];
let is_run = true;
let idPrefix = "greasyfork-enhance-";
no_run.forEach((suffix) => {
if (window.location.pathname.endsWith(suffix)) {
is_run = false;
}
});
if (!is_run) return;
// Config
let config_desc = {
"auto-hide-code": {
name: "Auto hide code",
value: true,
input: "current",
processor: "not",
formatter: "boolean"
},
"auto-hide-rows": {
name: "Min rows to hide",
value: 10,
processor: "int_range-1-"
},
"flat-layout": {
name: "Flat layout",
value: false,
input: "current",
processor: "not",
formatter: "boolean"
},
"animation": {
name: "Animation",
value: true,
input: "current",
processor: "not",
formatter: "boolean"
}
};
let config = GM_config(config_desc);
// CSS
const dynamicStyle = {
"flat-layout": `
.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;
}
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;
}
.showing-all-languages .badge-js, .showing-all-languages .badge-css, .script-type {
display: none;
}
ol.script-list > li > article > h2 > a.script-link {
white-space: nowrap;
}
ol.script-list > li > article > h2 > span.script-description {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
ol.script-list > li > article > div.script-meta-block {
width: 40%;
column-gap: 0;
}
ol.script-list > li[data-script-type="library"] > article > h2 {
width: 80%;
}
ol.script-list > li[data-script-type="library"] > article > div.script-meta-block {
width: 20%;
column-count: 1;
}
ol.script-list > li > article > div.script-meta-block > dl.inline-script-stats {
margin: 0;
}
ol.script-list > li > article > div.script-meta-block > dl.inline-script-stats > dd {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
`,
"animation": `
/* Toggle code animation */
pre > code {
/*will-change: height;*/
transition: height 0.5s ease-in-out 0s;
}
/* Adapted from animate.css - https://animate.style/ */
:root {
--animate-duration: 1s;
--animate-delay: 1s;
--animate-repeat: 1;
}
.animate__animated {
animation-duration: var(--animate-duration);
animation-fill-mode: both;
}
.animate__animated.animate__fastest {
animation-duration: calc(var(--animate-duration) / 3);
}
@keyframes tada {
from {
transform: scale3d(1, 1, 1);
}
10%, 20% {
transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg);
}
30%, 50%, 70%, 90% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg);
}
40%, 60%, 80% {
transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg);
}
to {
transform: scale3d(1, 1, 1);
}
}
.animate__tada {
animation-name: tada;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate__fadeIn {
animation-name: fadeIn;
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.animate__fadeOut {
-webkit-animation-name: fadeOut;
animation-name: fadeOut;
}`
};
// Functions
let body = document.querySelector("body");
function sanitify(s) {
// Remove emojis (such a headache)
s = s.replaceAll(/([\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2580-\u27BF]|\uD83E[\uDD10-\uDEFF]|\uFE0F)/g, "");
// Trim spaces and newlines
s = s.trim();
// Replace spaces
s = s.replaceAll(" ", "-");
s = s.replaceAll("%20", "-");
// No more multiple "-"
s = s.replaceAll(/-+/g, "-");
return s;
}
function process(node) { // Add anchor and assign id to given node; Add to outline. Return true if node is actually processed.
if (node.childElementCount > 1 || node.classList.length > 0) return false; // Ignore complex nodes
let text = node.textContent;
node.id = sanitify(text); // Assign id
// Add anchors
let node_ = document.createElement('a');
node_.className = 'anchor';
node_.href = '#' + node.id;
node.appendChild(node_);
let list_item = document.createElement("li");
outline.appendChild(list_item);
let link = document.createElement("a");
link.href = "#" + node.id;
link.text = text;
list_item.appendChild(link);
return true;
}
async function animate(node, animation) {
return new Promise((resolve, reject) => {
node.classList.add("animate__animated", "animate__" + animation);
if (node.getAnimations().length == 0) {
node.classList.remove("animate__animated", "animate__" + animation);
reject("No animation available");
}
node.addEventListener('animationend', e => {
e.stopPropagation();
node.classList.remove("animate__animated", "animate__" + animation);
resolve("Animation ended");
}, { once: true });
});
}
async function transition(node, height) {
return new Promise((resolve, reject) => {
node.style.height = height;
if (node.getAnimations().length == 0) {
resolve("No transition available");
}
node.addEventListener('transitionend', e => {
e.stopPropagation();
resolve("Transition ended");
}, { once: true });
});
}
function copyCode() {
let code = this.parentNode.nextElementSibling;
let text = code.textContent;
navigator.clipboard.writeText(text).then(() => {
this.textContent = "Copied!";
animate(this, "tada").then(() => {
this.textContent = "Copy code";
}, () => {
window.setTimeout(() => {
this.textContent = "Copy code";
}, 1000);
});
});
}
function toggleCode() {
let code = this.parentNode.nextElementSibling;
if (code.style.height == "0px") {
code.style.willChange = "height";
transition(code, code.getAttribute("data-height")).then(() => {
code.style.willChange = "";
});
animate(this, "fadeOut").then(() => {
this.textContent = "Hide code";
animate(this, "fadeIn");
}, () => {
this.textContent = "Hide code";
});
} else {
code.style.willChange = "height";
transition(code, "0px").then(() => {
code.style.willChange = "";
});
animate(this, "fadeOut").then(() => {
this.textContent = "Show code";
animate(this, "fadeIn");
}, () => {
this.textContent = "Show code";
});
}
}
function create_toolbar() {
let toolbar = document.createElement("div");
let copy = document.createElement("a");
let toggle = document.createElement("a");
toolbar.appendChild(copy);
toolbar.appendChild(toggle);
copy.textContent = "Copy code";
copy.className = "code-operation";
copy.title = "Copy code to clipboard";
copy.addEventListener("click", copyCode);
toggle.textContent = "Hide code";
toggle.classList.add("code-operation", "animate__fastest");
toggle.title = "Toggle code display";
toggle.addEventListener("click", toggleCode);
// Css
toolbar.className = "code-toolbar";
return toolbar;
}
function injectCSS(id, css) {
let style = document.createElement("style");
style.id = idPrefix + id;
style.textContent = css;
document.head.appendChild(style);
}
function cssHelper(id, enable) {
let current = document.getElementById(idPrefix + id);
if (current) {
current.disabled = !enable;
} else if (enable) {
injectCSS(id, dynamicStyle[id]);
}
}
// Basic css
injectCSS("basic", `
html {
scroll-behavior: smooth;
}
a.anchor::before {
content: "#";
}
a.anchor {
opacity: 0;
text-decoration: none;
padding: 0px 0.5em;
transition: all 0.25s ease-in-out;
}
h1:hover>a.anchor, h2:hover>a.anchor, h3:hover>a.anchor,
h4:hover>a.anchor, h5:hover>a.anchor, h6:hover>a.anchor {
opacity: 1;
transition: all 0.25s ease-in-out;
}
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;
}
div.code-toolbar {
display: flex;
gap: 1em;
}
a.code-operation {
cursor: pointer;
font-style: italic;
}
div.lum-lightbox {
z-index: 2;
}
div#float-buttons {
position: fixed;
bottom: 1em;
right: 1em;
display: flex;
flex-direction: column;
user-select: none;
z-index: 1;
}
aside.panel {
display: none;
}
.dynamic-opacity {
transition: opacity 0.2s ease-in-out;
opacity: 0.2;
}
.dynamic-opacity:hover {
opacity: 0.8;
}
input[type=file] {
border-style: dashed;
border-radius: 0.5em;
padding: 0.5em;
background: rgba(169, 169, 169, 0.4);
}
table {
border: 1px solid #8d8d8d;
border-collapse: collapse;
width: auto;
}
table td, table th {
padding: 0.5em 0.75em;
vertical-align: middle;
border: 1px solid #8d8d8d;
}
@media (any-hover: none) {
.dynamic-opacity {
opacity: 0.8;
}
.dynamic-opacity:hover {
opacity: 0.8;
}
}
@media screen and (min-width: 767px) {
aside.panel {
display: contents;
line-height: 1.5;
}
ul.outline {
position: sticky;
float: right;
padding: 0 0 0 0.5em;
margin: 0 0.5em;
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;
}
ul.outline > li {
overflow: hidden;
text-overflow: ellipsis;
}
ul.outline > li > a {
color: gray;
white-space: nowrap;
text-decoration: none;
}
}
pre > code {
overflow: hidden;
display: block;
}`);
// Aside panel & Anchors
let outline;
let is_script = /^\/[^\/]+\/scripts/;
let is_specific_script = /^\/[^\/]+\/scripts\/\d+/;
let is_disccussion = /^\/[^\/]+\/discussions/;
let path = window.location.pathname;
if ((!is_script.test(path) && !is_disccussion.test(path)) || is_specific_script.test(path)) {
let panel = document.createElement("aside");
panel.className = "panel";
body.insertBefore(panel, document.querySelector("body > div.width-constraint"));
let reference_node = document.querySelector("body > div.width-constraint > section");
outline = document.createElement("ul");
outline.classList.add("outline");
outline.classList.add("dynamic-opacity");
outline.style.top = reference_node ? getComputedStyle(reference_node).marginTop : "1em";
outline.style.marginTop = outline.style.top;
panel.appendChild(outline);
let flag = false;
document.querySelectorAll("body > div.width-constraint h1, h2, h3, h4, h5, h6").forEach((node) => {
flag = process(node) || flag; // Not `flag || process(node)`!
});
if (!flag) {
panel.remove();
}
}
// Navigate to hash
let hash = window.location.hash.slice(1);
if (hash) {
let ele = document.getElementById(decodeURIComponent(hash));
if (ele) {
ele.scrollIntoView();
}
}
// Buttons
let buttons = document.createElement("div");
buttons.id = "float-buttons";
let to_top = document.createElement("a");
to_top.classList.add("button");
to_top.classList.add("dynamic-opacity");
to_top.href = "#top";
to_top.text = "↑";
buttons.appendChild(to_top);
body.appendChild(buttons);
// Double click to get to top
body.addEventListener("dblclick", (e) => {
if (e.target === body) {
to_top.click();
}
});
// Fix current tab link
let tab = document.querySelector("ul#script-links > li.current");
if (tab) {
let link = document.createElement("a");
link.href = window.location.pathname;
let orig_child = tab.firstChild;
link.appendChild(orig_child);
tab.appendChild(link);
}
let parts = window.location.pathname.split("/");
if (parts.length <= 2 || (parts.length == 3 && parts[2] === '')) {
let banner = document.querySelector("header#main-header div#site-name");
let img = banner.querySelector("img");
let text = banner.querySelector("#site-name-text > h1");
let link1 = document.createElement("a");
link1.href = window.location.pathname;
img.parentNode.replaceChild(link1, img);
link1.appendChild(img);
let link2 = document.createElement("a");
link2.href = window.location.pathname;
link2.textContent = text.textContent;
text.textContent = "";
text.appendChild(link2);
}
// Toolbar for code blocks
let code_blocks = document.getElementsByTagName("pre");
let auto_hide = config["auto-hide-code"];
let auto_hide_rows = config["auto-hide-rows"];
for (let code_block of code_blocks) {
if (code_block.firstChild.tagName === "CODE") {
let height = getComputedStyle(code_block.firstChild).getPropertyValue("height");
code_block.firstChild.style.height = height;
code_block.firstChild.setAttribute("data-height", height);
code_block.insertAdjacentElement("afterbegin", create_toolbar());
}
}
// Auto hide code blocks
function autoHide() {
if (!auto_hide) {
for (let code_block of code_blocks) {
let toggle = code_block.firstChild.lastChild;
if (toggle.textContent === "Show code") {
toggle.click(); // Click the toggle button
}
}
} else {
for (let code_block of code_blocks) {
let m = code_block.lastChild.textContent.match(/\n/g);
let rows = m ? m.length : 0;
let toggle = code_block.firstChild.lastChild;
let hidden = toggle.textContent === "Show code";
if (rows >= auto_hide_rows && !hidden || rows < auto_hide_rows && hidden) {
code_block.firstChild.lastChild.click(); // Click the toggle button
}
}
}
}
document.addEventListener("readystatechange", (e) => {
if (e.target.readyState === "complete") {
autoHide();
}
}, {once: true});
// Initialize css
for (let prop in dynamicStyle) {
cssHelper(prop, config[prop]);
}
// Dynamically respond to config changes
let callbacks = {
"auto-hide-code": (after) => {
auto_hide = after;
autoHide();
},
"auto-hide-rows": (after) => {
auto_hide_rows = after;
autoHide();
}
};
window.addEventListener(GM_config_event, e => {
if (e.detail.type === "set") {
let callback = callbacks[e.detail.prop];
if (callback && (e.detail.before !== e.detail.after)) {
callback(e.detail.after);
} else if (e.detail.prop in dynamicStyle) {
cssHelper(e.detail.prop, e.detail.after);
}
}
});
})();