// ==UserScript==
// @name Better Greasy Fork
// @name:pt-BR Greasy Fork Aprimorado
// @name:zh-CN 更好的 Greasy Fork
// @name:zh-TW 更好的 Greasy Fork
// @name:en Better Greasy Fork
// @name:es Greasy Fork Mejorado
// @name:ja 改良版 Greasy Fork
// @name:ko 향상된 Greasy Fork
// @name:de Verbesserter Greasy Fork
// @name:fr Greasy Fork Amélioré
// @namespace https://github.com/0H4S
// @version 1.2
// @description Enhances Greasy Fork with useful features: shows the script icon next to the title, adds a Markdown editor (for comments/descriptions), and a button to download the script as a ".user.js" file from the code page. It also enriches script pages with author customizations via metadata, such as accent colors, copyright info, and social icons.
// @description:pt-BR Aprimora o Greasy Fork: exibe o ícone do script ao lado do título, adiciona um editor Markdown (para comentários/descrições) e um botão para baixar o script como ".user.js" na página de código. Também enriquece as páginas com personalizações de autor via metadados, como cores de destaque, copyright e ícones sociais.
// @description:zh-CN 为 Greasy Fork 增强多项实用功能:在标题旁显示脚本图标,在文本编辑器(用于评论和描述)中加入 Markdown 格式化工具,并在“代码”页面新增下载按钮,可将脚本直接下载为“.user.js”文件。此外,通过元数据为作者提供新的自定义选项,丰富脚本页面,显示高亮颜色、版权信息和社交图标。
// @description:zh-TW 為 Greasy Fork 增強多項實用功能:在標題旁顯示腳本圖示,在文字編輯器(用於留言與說明)中加入 Markdown 格式化工具,並在「程式碼」頁面新增下載按鈕,可將腳本直接下載為「.user.js」檔案。此外,透過元資料為作者提供新的自訂選項,豐富腳本頁面,顯示重點色、版權資訊與社群圖示。
// @description:en Enhances Greasy Fork with useful features: shows the script icon next to the title, adds a Markdown editor (for comments/descriptions), and a button to download the script as a ".user.js" file from the code page. It also enriches script pages with author customizations via metadata, such as accent colors, copyright info, and social icons.
// @description:es Mejora Greasy Fork: muestra el icono del script junto al título, añade un editor Markdown (para comentarios/descripciones) y un botón para descargar el script como ".user.js" en la página de código. También enriquece las páginas con personalizaciones para autores vía metadatos, mostrando colores de realce, copyright e iconos sociales.
// @description:ja Greasy Fork を便利な機能で強化します:タイトル横にスクリプトのアイコンを表示し、テキストエディタ(コメントや説明用)に Markdown 整形ツールを追加し、「コード」ページにスクリプトを直接「.user.js」としてダウンロードできる新しいダウンロードボタンを作成します。さらに、メタデータを通じて作者向けのカスタマイズオプションを追加し、ハイライトカラー、著作権情報、SNS アイコンを表示してスクリプトページを充実させます。
// @description:ko Greasy Fork에 여러 유용한 기능을 추가합니다: 제목 옆에 스크립트 아이콘을 표시하고, 텍스트 편집기(댓글 및 설명용)에 Markdown 서식 도구를 추가하며, '코드' 페이지에 스크립트를 '.user.js' 파일로 직접 다운로드할 수 있는 새 다운로드 버튼을 만듭니다. 또한 메타데이터를 통해 저자를 위한 맞춤 설정 옵션을 제공해 강조 색상, 저작권 정보, 소셜 아이콘을 표시합니다.
// @description:de Verbessert Greasy Fork: zeigt das Skript-Symbol neben dem Titel, fügt einen Markdown-Editor (für Kommentare/Beschreibungen) und einen Button zum direkten Download als ".user.js"-Datei auf der Code-Seite hinzu. Erweitert Skriptseiten zudem um Autoren-Anpassungen via Metadaten wie Akzentfarben, Copyright-Infos & Social-Icons.
// @description:fr Améliore Greasy Fork : affiche l'icône du script à côté du titre, ajoute un éditeur Markdown (pour commentaires/descriptions) et un bouton pour télécharger le script en ".user.js" sur la page « Code ». Enrichit aussi les pages de script avec des personnalisations d'auteur via métadonnées, comme les couleurs d'accent, le copyright et les icônes sociales.
// @author OHAS
// @license CC-BY-NC-ND-4.0
// @match https://greasyfork.org/*
// @icon https://gist.githubusercontent.com/0H4S/ff8b21d291d9cd8cdcf4cf1a0f96748c/raw/icon.svg
// @require https://update.greasyfork.org/scripts/549920/Script%20Notifier.js
// @resource customCSS https://gist.githubusercontent.com/0H4S/ff8b21d291d9cd8cdcf4cf1a0f96748c/raw/styles.css
// @resource iconsJSON https://gist.githubusercontent.com/0H4S/ff8b21d291d9cd8cdcf4cf1a0f96748c/raw/icons.json
// @resource translationsJSON https://gist.githubusercontent.com/0H4S/ff8b21d291d9cd8cdcf4cf1a0f96748c/raw/translations.json
// @connect gist.githubusercontent.com
// @connect update.greasyfork.org
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @grant GM_getResourceText
// @grant GM_registerMenuCommand
// @run-at document-idle
// @compatible chrome
// @compatible firefox
// @compatible edge
// @bgf-colorLT #0059ffff
// @bgf-colorDT #ffffffff
// @bgf-copyright [2025 OHAS. All Rights Reserved.](https://gist.github.com/0H4S/ae2fa82957a089576367e364cbf02438)
// @bgf-compatible brave, mobile
// @bgf-social https://github.com/0H4S, https://www.instagram.com/o_h_a_s
// @contributionURL https://linktr.ee/0H4S
// @contributionAmount 1
// ==/UserScript==
(function () {
'use strict';
// ================
// #region GLOBAL
// ================
if (window.top !== window.self) {
return;
}
const SCRIPT_CONFIG = {
notificationsUrl: 'https://gist.githubusercontent.com/0H4S/1eee8eb439b554860274686143eda3f9/raw/better_greasy_fork.notifications.json',
scriptVersion: '1.2',
};
const notifier = new ScriptNotifier(SCRIPT_CONFIG);
notifier.run();
const CACHE_KEY = 'Values';
const translationsJSONString = GM_getResourceText("translationsJSON");
const translations = JSON.parse(translationsJSONString);
const icons = JSON.parse(GM_getResourceText("iconsJSON"));
const myCss = GM_getResourceText("customCSS");
GM_addStyle(myCss);
function capitalizeCompatItem(item) {
return item.replace(/\b\w/g, char => char.toUpperCase());
}
let currentLang = 'en';
let languageModal = null;
const LANG_STORAGE_KEY = 'UserScriptLang';
function getTranslation(key) {
return translations[currentLang] ?.[key] || translations.en[key];
}
async function determineLanguage() {
const savedLang = await GM_getValue(LANG_STORAGE_KEY);
if (savedLang && translations[savedLang]) {
currentLang = savedLang;
return;
}
const browserLang = (navigator.language || navigator.userLanguage).toLowerCase();
if (browserLang.startsWith('pt')) currentLang = 'pt-BR';
else if (browserLang.startsWith('es')) currentLang = 'es';
else if (browserLang.startsWith('zh')) currentLang = 'zh-CN';
else currentLang = 'en';
}
function registerLanguageMenu() {
GM_registerMenuCommand(getTranslation('languageSettings'), () => {
showModal(languageModal);
});
}
function registerForceUpdateMenu() {
GM_registerMenuCommand(getTranslation('force_update'), forceUpdate);
}
function showModal(modal) {
if (!modal) return;
modal.style.display = 'flex';
setTimeout(() => {
const box = modal.querySelector('.lang-modal-box');
box.style.opacity = '1';
box.style.transform = 'scale(1)';
}, 10);
}
function hideModal(modal) {
if (!modal) return;
const box = modal.querySelector('.lang-modal-box');
box.style.opacity = '0';
box.style.transform = 'scale(0.95)';
setTimeout(() => {
modal.style.display = 'none';
}, 200);
}
function createLanguageModal() {
const overlay = document.createElement('div');
overlay.className = 'lang-modal-overlay';
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
hideModal(overlay);
}
});
const box = document.createElement('div');
box.className = 'lang-modal-box';
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'lang-modal-buttons';
Object.keys(translations).forEach(langKey => {
const btn = document.createElement('button');
btn.textContent = translations[langKey].langName;
btn.onclick = async () => {
await GM_setValue(LANG_STORAGE_KEY, langKey);
window.location.reload();
};
buttonsContainer.appendChild(btn);
});
box.appendChild(buttonsContainer);
overlay.appendChild(box);
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
function applyTheme(isDark) {
box.classList.toggle('dark-theme', isDark);
box.classList.toggle('light-theme', !isDark);
}
applyTheme(mediaQuery.matches);
mediaQuery.addEventListener('change', e => applyTheme(e.matches));
return overlay;
}
async function forceUpdate() {
alert(getTranslation('force_update_alert'));
await GM_deleteValue(CACHE_KEY);
window.location.reload();
}
// ================
// #region ESTILIZAR
// ================
function isScriptPage() {
const path = window.location.pathname;
return /^\/([a-z]{2}(-[A-Z]{2})?\/)?scripts\/\d+-[^/]+$/.test(path);
}
function addAdditionalInfoSeparator() {
const additionalInfo = document.getElementById('additional-info');
if (additionalInfo && !additionalInfo.previousElementSibling?.matches('hr.bgs-info-separator')) {
const hr = document.createElement('hr');
hr.className = 'bgs-info-separator';
additionalInfo.before(hr);
}
}
function highlightScriptDescription() {
const descriptionElements = document.querySelectorAll('#script-description, .script-description.description');
descriptionElements.forEach(element => {
const scriptLink = element.closest('article, li')?.querySelector('a.script-link');
const path = scriptLink ? normalizeScriptPath(new URL(scriptLink.href).pathname) : normalizeScriptPath(window.location.pathname);
if (element && element.parentElement.tagName !== 'BLOCKQUOTE') {
const blockquoteWrapper = document.createElement('blockquote');
blockquoteWrapper.className = 'script-description-blockquote';
if (path) {
blockquoteWrapper.dataset.bgfPath = path;
}
element.parentNode.insertBefore(blockquoteWrapper, element);
blockquoteWrapper.appendChild(element);
}
});
}
function makeDiscussionClickable() {
document.querySelectorAll('.discussion-list-container').forEach(container => {
container.removeEventListener('click', handleDiscussionClick);
container.addEventListener('click', handleDiscussionClick);
});
}
function handleDiscussionClick(e) {
if (e.target.tagName === 'A' ||
e.target.closest('a') ||
e.target.closest('.user-link') ||
e.target.closest('.badge-author') ||
e.target.closest('.rating-icon')) {
return;
}
const discussionLink = this.querySelector('.discussion-title');
if (discussionLink && discussionLink.href) {
window.location.href = discussionLink.href;
}
}
function applySyntaxHighlighting() {
document.querySelectorAll('pre code').forEach(block => {
if (block.dataset.highlighted === 'true') { return; }
const code = block.textContent;
block.innerHTML = highlight(code);
block.dataset.highlighted = 'true';
});
}
function escapeHtml(str) {
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function highlight(code) {
const keywords = new Set(['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'of', 'in', 'async', 'await', 'try', 'catch', 'new', 'import', 'export', 'from', 'class', 'extends', 'super', 'true', 'false', 'null', 'undefined', 'document', 'window']);
const tokens = [];
let cursor = 0;
const tokenDefinitions = [
{ type: 'url', regex: /^(https?:\/\/[^\s"'`<>]+)/ },
{ type: 'comment-special', regex: /^(\/\/[^\r\n]*)/ },
{ type: 'comment', regex: /^(\/\*[\s\S]*?\*\/|<!--[\s\S]*?-->)/ },
{ type: 'string', regex: /^(`(?:\\.|[^`])*`|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')/ },
{ type: 'tag-punctuation', regex: /^(<\/?|\/>|>)/ },
{ type: 'tag-name', regex: /^([\w-]+)/, context: (t) => { const l=t[t.length-1]; return l&&l.type==='tag-punctuation'&&l.content.startsWith('<') }},
{ type: 'attribute', regex: /^([\w-]+)/, context: (t) => { for(let i=t.length-1;i>=0;i--){const n=t[i];if(n.type==='tag-punctuation'&&n.content.includes('>'))return!1;if(n.type==='tag-name')return!0;if(n.type==='whitespace')continue}return!1 }},
{ type: 'regex', regex: /^(\/(?!\*)(?:[^\r\n/\\]|\\.)+\/[gimyus]*)/ },
{ type: 'number', regex: /^\b-?(\d+(\.\d+)?)\b/ },
{ type: 'keyword', regex: new RegExp(`^\\b(${Array.from(keywords).join('|')})\\b`) },
{ type: 'function', regex: /^([a-zA-Z_][\w_]*)(?=\s*\()/ },
{ type: 'property', regex: /^\.([a-zA-Z_][\w_]*)/ },
{ type: 'operator', regex: /^(==?=?|!=?=?|=>|[+\-*/%&|^<>]=?|\?|:|=)/ },
{ type: 'punctuation', regex: /^([,;(){}[\]])/ },
{ type: 'whitespace', regex: /^\s+/ },
{ type: 'unknown', regex: /^./ },
];
let processedCode = escapeHtml(code);
while (cursor < processedCode.length) {
let matched = false;
for (const def of tokenDefinitions) {
if (def.context && !def.context(tokens)) { continue; }
const match = def.regex.exec(processedCode.slice(cursor));
if (match) {
const content = match[0];
if (def.type === 'function' && keywords.has(content)) { continue; }
tokens.push({ type: def.type, content });
cursor += content.length;
matched = true;
break;
}
}
if (!matched) {
tokens.push({ type: 'unknown', content: processedCode[cursor] });
cursor++;
}
}
for (let i = 0; i < tokens.length; i++) {
if (tokens[i].type === 'string') {
let nextToken = null;
for(let j=i+1;j<tokens.length;j++){if(tokens[j].type!=='whitespace'){nextToken=tokens[j];break}}
if (nextToken && nextToken.content === ':') { tokens[i].type = 'json-key'; }
}
}
return tokens.map(token => {
if (['whitespace', 'unknown', 'url'].includes(token.type)) return token.content;
if (token.type === 'property') return `<span class="sh-punctuation">.</span><span class="sh-property">${token.content.slice(1)}</span>`;
return `<span class="sh-${token.type}">${token.content}</span>`;
}).join('');
}
// ================
// #region ÍCONES
// ================
let iconCache;
const processedKeys = new Set();
async function saveCache() {
await GM_setValue(CACHE_KEY, iconCache);
}
function normalizeScriptPath(pathname) {
let withoutLocale = pathname.replace(/^\/[a-z]{2}(?:-[A-Z]{2})?\//, '/');
const match = withoutLocale.match(/^\/scripts\/\d+-.+?(?=\/|$)/);
return match ? match[0] : null;
}
function extractScriptIdFromNormalizedPath(normalized) {
const match = normalized.match(/\/scripts\/(\d+)-/);
return match ? match[1] : null;
}
function createIconElement(src, isHeader = false) {
const img = document.createElement('img');
img.src = src;
img.alt = '';
if (isHeader) {
img.style.cssText = `
width: 80px;
height: 80px;
margin-right: 10px;
vertical-align: middle;
border-radius: 4px;
object-fit: contain;
pointer-events: none;
`;
} else {
img.style.cssText = `
width: 40px;
height: 40px;
margin-right: 8px;
vertical-align: middle;
border-radius: 3px;
object-fit: contain;
pointer-events: none;
`;
}
img.loading = 'lazy';
return img;
}
function extractMetadataFromContent(content) {
if (typeof content !== 'string') return {};
const metadata = {};
const lines = content.split('\n');
const supportedTags = new Set([
'@icon', '@bgf-colorLT', '@bgf-colorDT', '@bgf-compatible',
'@bgf-copyright', '@bgf-social'
]);
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('// ==/UserScript==')) break;
if (!trimmedLine.startsWith('// @')) continue;
const match = trimmedLine.match(/\/\/\s*(@[a-zA-Z0-9-]+)\s+(.+)/);
if (!match) continue;
const key = match[1];
let value = match[2].trim();
if (supportedTags.has(key) && !metadata.hasOwnProperty(key)) {
if (key === '@bgf-colorLT' || key === '@bgf-colorDT') {
const colorRegex = /(#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\s*\([^)]+\))/;
const colorMatch = value.match(colorRegex);
if (colorMatch) {
value = colorMatch[0];
} else {
value = value.split(',')[0].trim();
}
}
metadata[key] = value;
}
}
return metadata;
}
function isValidIconUrl(url) {
return url && (url.startsWith('http') || url.startsWith(''));
}
async function processScript(normalizedPath, targetElement, isHeader = false) {
if (processedKeys.has(normalizedPath) && isHeader) {
applyBfgFeatures(iconCache[normalizedPath]);
}
if (processedKeys.has(normalizedPath) && !isHeader) {
const cached = iconCache[normalizedPath];
if (cached && isValidIconUrl(cached.iconUrl)) {
targetElement.prepend(createIconElement(cached.iconUrl, isHeader));
}
return;
}
processedKeys.add(normalizedPath);
const cached = iconCache[normalizedPath];
const now = Date.now();
const applyColorToBlockquote = (metadata) => {
const blockquotes = document.querySelectorAll(`blockquote.script-description-blockquote[data-bgf-path="${normalizedPath}"]`);
if (blockquotes.length === 0) return;
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const colorToApply = isDarkMode ? metadata.bgfColorDT : metadata.bgfColorLT;
blockquotes.forEach(bq => {
if (colorToApply) {
bq.style.setProperty('border-left-color', colorToApply, 'important');
} else {
bq.style.removeProperty('border-left-color');
}
});
};
if (cached && now - cached.ts < 30 * 24 * 60 * 60 * 1000) {
if (isValidIconUrl(cached.iconUrl)) {
targetElement.prepend(createIconElement(cached.iconUrl, isHeader));
}
applyColorToBlockquote(cached);
if (isHeader) {
applyBfgFeatures(cached);
}
return;
}
const scriptId = extractScriptIdFromNormalizedPath(normalizedPath);
if (!scriptId) {
iconCache[normalizedPath] = { ts: now };
await saveCache();
return;
}
const scriptUrl = `https://update.greasyfork.org/scripts/${scriptId}.js`;
GM_xmlhttpRequest({
method: 'GET',
url: scriptUrl,
timeout: 6000,
onload: async function (res) {
if (typeof res.responseText !== 'string') {
iconCache[normalizedPath] = { ts: now };
await saveCache();
return;
}
const rawMetadata = extractMetadataFromContent(res.responseText);
const metadata = {
iconUrl: rawMetadata['@icon'] || null,
bgfColorLT: rawMetadata['@bgf-colorLT'] || null,
bgfColorDT: rawMetadata['@bgf-colorDT'] || null,
bgfCompatible: rawMetadata['@bgf-compatible'] || null,
bgfCopyright: rawMetadata['@bgf-copyright'] || null,
bgfSocial: rawMetadata['@bgf-social'] || null,
ts: now
};
iconCache[normalizedPath] = metadata;
await saveCache();
if (isValidIconUrl(metadata.iconUrl)) {
targetElement.prepend(createIconElement(metadata.iconUrl, isHeader));
}
applyColorToBlockquote(metadata);
if (isHeader) {
applyBfgFeatures(metadata);
}
},
onerror: async function () {
iconCache[normalizedPath] = { ts: now };
await saveCache();
}
});
}
function handleScriptLink(linkEl) {
if (linkEl._handled) return;
linkEl._handled = true;
const href = linkEl.getAttribute('href');
if (!href || !href.startsWith('/')) return;
try {
const url = new URL(href, window.location.origin);
const normalized = normalizeScriptPath(url.pathname);
if (!normalized) return;
setTimeout(() => processScript(normalized, linkEl, false), 0);
} catch (e) {}
}
function handleMainHeaderH2() {
const headers = document.querySelectorAll('header');
for (const header of headers) {
const h2 = header.querySelector('h2');
const desc = header.querySelector('p.script-description');
if (h2 && desc && !h2._handled) {
h2._handled = true;
const normalized = normalizeScriptPath(window.location.pathname);
if (!normalized) return;
setTimeout(() => processScript(normalized, h2, true), 0);
break;
}
}
}
function processIconElements() {
document.querySelectorAll('a.script-link:not([data-icon-processed])')
.forEach(el => {
el.setAttribute('data-icon-processed', '1');
handleScriptLink(el);
});
handleMainHeaderH2();
}
// ================
// #region RECURSOS BFG
// ================
function applyBfgFeatures(metadata) {
if (!metadata) return;
applyBfgCompatibility(metadata.bgfCompatible);
applyBfgCopyright(metadata.bgfCopyright);
applyBfgSocial(metadata.bgfSocial);
}
function applyBfgCompatibility(compatValue) {
if (!compatValue) return;
const compatDd = document.querySelector('dd.script-show-compatibility');
if (!compatDd) {
return;
}
let compatContainer = compatDd.querySelector('span');
if (!compatContainer) {
compatContainer = document.createElement('span');
compatDd.innerHTML = '';
compatDd.appendChild(compatContainer);
}
const compatItems = compatValue.split(',').map(item => item.trim().toLowerCase());
compatItems.forEach(item => {
if (!icons[item] || compatContainer.querySelector(`.bgf-compat-${item}`)) {
return;
}
const img = document.createElement('img');
img.className = `browser-compatible bgf-compat-${item}`;
const displayName = capitalizeCompatItem(item);
img.alt = `${getTranslation('compatible_with')} ${displayName}`;
img.title = `${getTranslation('compatible_with')} ${displayName}`;
img.style.marginLeft = '1px';
img.src = `data:image/svg+xml;utf8,${encodeURIComponent(icons[item])}`;
compatContainer.appendChild(img);
});
}
function reapplyAllBlockquoteColors() {
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const allBlockquotes = document.querySelectorAll('blockquote.script-description-blockquote[data-bgf-path]');
allBlockquotes.forEach(bq => {
const path = bq.dataset.bgfPath;
if (!path || !iconCache[path]) return;
const metadata = iconCache[path];
const colorToApply = isDarkMode ? metadata.bgfColorDT : metadata.bgfColorLT;
if (colorToApply) {
bq.style.setProperty('border-left-color', colorToApply, 'important');
} else {
bq.style.removeProperty('border-left-color');
}
});
}
function setupThemeChangeListener() {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', reapplyAllBlockquoteColors);
}
function applyBfgCopyright(copyrightValue) {
if (!copyrightValue || document.querySelector('.script-show-copyright')) return;
const copyrightRegex = /\[(.{1,50})\]\((https:\/\/gist\.github\.com\/[^)]+)\)/;
const match = copyrightValue.match(copyrightRegex);
if (!match) return;
const licenseDd = document.querySelector('dd.script-show-license');
if (!licenseDd) return;
const text = match[1];
const url = match[2];
const copyrightDt = document.createElement('dt');
copyrightDt.className = 'script-show-copyright';
copyrightDt.innerHTML = '<span>Copyright</span>';
const copyrightDd = document.createElement('dd');
copyrightDd.className = 'script-show-copyright';
copyrightDd.style.alignSelf = 'center';
const link = document.createElement('a');
link.href = url;
link.textContent = text;
link.target = '_blank';
link.rel = 'noopener noreferrer';
const span = document.createElement('span');
span.appendChild(link);
copyrightDd.appendChild(span);
licenseDd.after(copyrightDt, copyrightDd);
}
function applyBfgSocial(socialValue) {
if (!socialValue || document.querySelector('.script-show-social')) return;
const authorDd = document.querySelector('dd.script-show-author');
if (!authorDd) return;
const socialDomainMap = {
'instagram.com': { icon: icons.instagram, name: 'Instagram' },
'facebook.com': { icon: icons.facebook, name: 'Facebook' },
'x.com': { icon: icons.x, name: 'X / Twitter' },
'youtube.com': { icon: icons.youtube, name: 'YouTube' },
'bilibili.com': { icon: icons.bilibili, name: 'Bilibili' },
'tiktok.com': { icon: icons.tiktok, name: 'TikTok' },
'douyin.com': { icon: icons.tiktok, name: 'Douyin' },
'github.com': { icon: icons.github, name: 'GitHub' },
'linkedin.com': { icon: icons.linkedin, name: 'LinkedIn' },
};
const urls = socialValue.split(',').map(url => url.trim());
const validLinks = [];
let tiktokFamilyProcessed = false;
urls.forEach(url => {
try {
const domain = new URL(url).hostname.replace('www.', '');
if (socialDomainMap[domain]) {
if (domain === 'tiktok.com' || domain === 'douyin.com') {
if (tiktokFamilyProcessed) return;
tiktokFamilyProcessed = true;
}
validLinks.push({ url, ...socialDomainMap[domain] });
}
} catch (e) {}
});
if (validLinks.length === 0) return;
const socialDt = document.createElement('dt');
socialDt.className = 'script-show-social';
socialDt.innerHTML = '<span>Social</span>';
const socialDd = document.createElement('dd');
socialDd.className = 'script-show-social';
socialDd.style.cssText = 'display: flex; gap: 8px; align-items: center; align-self: center;';
validLinks.forEach(linkInfo => {
const link = document.createElement('a');
link.href = linkInfo.url;
link.title = linkInfo.name;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.innerHTML = linkInfo.icon;
const svg = link.querySelector('svg');
if (svg) {
svg.style.width = '20px';
svg.style.height = '20px';
svg.style.verticalAlign = 'middle';
}
socialDd.appendChild(link);
});
authorDd.after(socialDt, socialDd);
}
// ================
// #region EDITOR MD
// ================
function insertText(textarea, prefix, suffix = '', placeholder = '') {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selected = textarea.value.substring(start, end);
const text = selected || placeholder;
textarea.setRangeText(prefix + text + suffix, start, end, selected ? 'end' : 'select');
textarea.focus();
}
function createToolbarButton(def) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'txt-editor-toolbar-button';
btn.dataset.tooltip = def.title;
btn.innerHTML = def.icon || def.label;
btn.addEventListener('click', e => {
e.preventDefault();
def.action();
});
return btn;
}
function createTextStyleEditor(textarea) {
if (textarea.dataset.editorApplied) return;
textarea.dataset.editorApplied = 'true';
const container = document.createElement('div');
container.className = 'txt-editor-container';
const toolbar = document.createElement('div');
toolbar.className = 'txt-editor-toolbar';
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
function applyTheme(isDark) {
container.classList.toggle('dark-theme', isDark);
container.classList.toggle('light-theme', !isDark);
}
applyTheme(mediaQuery.matches);
mediaQuery.addEventListener('change', e => applyTheme(e.matches));
const tools = [
{ type: 'select', title: getTranslation('titles'), options: { 'H1': '# ', 'H2': '## ', 'H3': '### ', 'H4': '#### ', 'H5': '##### ', 'H6': '###### ' }, action: (val) => insertText(textarea, val, '', getTranslation('title_placeholder')) },
{ type: 'divider' },
{ title: getTranslation('bold'), icon: icons.bold, action: () => insertText(textarea, '**', '**', getTranslation('bold_placeholder')) },
{ title: getTranslation('italic'), icon: icons.italic, action: () => insertText(textarea, '*', '*', getTranslation('italic_placeholder')) },
{ title: getTranslation('underline'), icon: icons.underline, action: () => insertText(textarea, '<u>', '</u>', getTranslation('underline_placeholder')) },
{ title: getTranslation('strikethrough'), icon: icons.strikethrough, action: () => insertText(textarea, '~~', '~~', getTranslation('strikethrough_placeholder')) },
{ type: 'divider' },
{ title: getTranslation('unordered_list'), icon: icons.ul, action: () => { const start = textarea.selectionStart, end = textarea.selectionEnd, selection = textarea.value.substring(start, end); textarea.setRangeText(selection ? selection.split('\n').map(line => line.trim() === '' ? '' : '- ' + line).join('\n') : '\n- ' + getTranslation('list_item_placeholder'), start, end, 'select'); textarea.focus(); } },
{ title: getTranslation('ordered_list'), icon: icons.ol, action: () => { const start = textarea.selectionStart, end = textarea.selectionEnd, selection = textarea.value.substring(start, end); if (selection) { let counter = 1; textarea.setRangeText(selection.split('\n').map(line => line.trim() === '' ? '' : (counter++) + '. ' + line).join('\n'), start, end, 'select'); } else insertText(textarea, '\n1. ', '', getTranslation('list_item_placeholder')); textarea.focus(); } },
{ type: 'divider' },
{ title: getTranslation('quote'), icon: icons.quote, action: () => { const start = textarea.selectionStart, end = textarea.selectionEnd, selection = textarea.value.substring(start, end); textarea.setRangeText(selection ? selection.split('\n').map(line => line.trim() === '' ? '' : '> ' + line).join('\n') : '\n> ' + getTranslation('quote_placeholder'), start, end, 'select'); textarea.focus(); } },
{ title: getTranslation('inline_code'), icon: icons.code, action: () => insertText(textarea, '`', '`', getTranslation('inline_code_placeholder')) },
{ title: getTranslation('code_block'), label: icons.code_block, action: () => insertText(textarea, '\n```\n', '\n```\n', getTranslation('code_block_placeholder')) },
{ title: getTranslation('horizontal_line'), icon: icons.hr, action: () => insertText(textarea, '\n---\n') },
{ type: 'divider' },
{ title: getTranslation('link'), icon: icons.link, action: () => { const url = prompt(getTranslation('prompt_insert_url'), "https://"); if (url) insertText(textarea, '[', `](${url})`, getTranslation('link_text_placeholder')); } },
{ title: getTranslation('image'), icon: icons.image, action: () => { const url = prompt(getTranslation('prompt_insert_image_url'), "https://"); if (url) insertText(textarea, ``); } },
{ title: getTranslation('table'), icon: icons.table, action: () => { const cols = parseInt(prompt(getTranslation('prompt_columns'), "3"), 10) || 3; const rows = parseInt(prompt(getTranslation('prompt_rows'), "2"), 10) || 2; let table = '\n| ' + Array(cols).fill(getTranslation('table_header_placeholder')).join(' | ') + ' |\n'; table += '| ' + Array(cols).fill('---').join(' | ') + ' |\n'; for (let i = 0; i < rows; i++) { table += '| ' + Array(cols).fill(getTranslation('table_cell_placeholder')).join(' | ') + ' |\n'; } insertText(textarea, table); } },
{ title: getTranslation('video'), icon: icons.video, action: () => { const url = prompt(getTranslation('prompt_insert_video_url')); if (!url) return; let src = ''; if (url.includes('youtube.com/watch?v=')) src = `https://www.youtube.com/embed/${new URL(url).searchParams.get('v')}`; else if (url.includes('youtu.be/')) src = `https://www.youtube.com/embed/${new URL(url).pathname.substring(1)}`; else if (url.includes('bilibili.com/video/')) src = `https://player.bilibili.com/player.html?bvid=${new URL(url).pathname.split('/')[2]}`; if (src) insertText(textarea, `<iframe src="${src}" allowfullscreen></iframe>`); else alert(getTranslation('alert_invalid_video_url')); } },
{ type: 'divider' },
{ title: getTranslation('subscript'), label: icons.subscript, action: () => insertText(textarea, '<sub>', '</sub>', getTranslation('subscript_placeholder')) },
{ title: getTranslation('superscript'), label: icons.superscript, action: () => insertText(textarea, '<sup>', '</sup>', getTranslation('superscript_placeholder')) },
{ title: getTranslation('highlight'), label: icons.highlight, action: () => insertText(textarea, '<mark>', '</mark>', getTranslation('highlight_placeholder')) },
{ title: getTranslation('keyboard'), label: icons.keyboard, action: () => insertText(textarea, '<kbd>', '</kbd>', getTranslation('keyboard_placeholder')) },
{ title: getTranslation('abbreviation'), label: icons.abbreviation, action: () => { const title = prompt(getTranslation('prompt_abbreviation_meaning')); if (title) insertText(textarea, `<abbr title="${title}">`, `</abbr>`, getTranslation('abbreviation_placeholder')); } },
{ type: 'color-picker' }
];
tools.forEach(tool => {
if (tool.type === 'divider') {
const div = document.createElement('div');
div.className = 'txt-editor-toolbar-divider';
toolbar.appendChild(div);
} else if (tool.type === 'select') {
const container = document.createElement('span');
container.className = 'txt-editor-toolbar-button';
container.dataset.tooltip = tool.title;
container.style.position = 'relative';
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.justifyContent = 'center';
container.innerHTML = icons.h;
const select = document.createElement('select');
select.className = 'txt-editor-toolbar-select';
select.style.cssText = ` -webkit-appearance: none; appearance: none; background: transparent; border: none; color: transparent; position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: pointer; `;
const placeholderOpt = document.createElement('option');
placeholderOpt.value = '';
placeholderOpt.textContent = '';
placeholderOpt.disabled = true;
placeholderOpt.selected = true;
placeholderOpt.style.display = 'none';
select.appendChild(placeholderOpt);
Object.keys(tool.options).forEach(key => {
const opt = document.createElement('option');
opt.value = tool.options[key];
opt.textContent = key;
select.appendChild(opt);
});
select.addEventListener('change', () => {
if (select.value) tool.action(select.value);
select.selectedIndex = 0;
});
container.appendChild(select);
toolbar.appendChild(container);
} else if (tool.type === 'color-picker') {
const colorContainer = document.createElement('div');
colorContainer.className = 'txt-color-picker-container';
const input = document.createElement('input');
input.type = 'color';
input.className = 'txt-color-picker-input';
input.value = "#58a6ff";
const colorBtn = createToolbarButton({
title: getTranslation('text_color'),
label: icons.text_color,
action: () => insertText(textarea, `<span style="color: ${input.value};">`, '</span>', getTranslation('colored_text_placeholder'))
});
const bgBtn = createToolbarButton({
title: getTranslation('background_color'),
label: icons.background_color,
action: () => insertText(textarea, `<span style="background-color: ${input.value};">`, '</span>', getTranslation('colored_background_placeholder'))
});
colorContainer.append(input, colorBtn, bgBtn);
toolbar.appendChild(colorContainer);
} else {
toolbar.appendChild(createToolbarButton(tool));
}
});
textarea.parentNode.insertBefore(container, textarea);
container.append(toolbar, textarea);
}
function applyToAllTextareas() {
const textareas = document.querySelectorAll('textarea:not(#script_version_code):not([data-editor-applied])');
textareas.forEach(createTextStyleEditor);
}
function enableSourceEditorCheckbox() {
const enableCheckbox = () => {
const checkbox = document.getElementById('enable-source-editor-code');
if (checkbox && !checkbox.checked) {
checkbox.checked = true;
const event = new Event('change', {
bubbles: true
});
checkbox.dispatchEvent(event);
}
};
enableCheckbox();
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
const checkbox = document.getElementById('enable-source-editor-code');
if (checkbox) {
enableCheckbox();
observer.disconnect();
break;
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function isMarkdownPage() {
const path = window.location.pathname;
const markdownSegments = [
'/new',
'/edit',
'/feedback',
'/discussions'
];
if (path.includes('/sets/')) {
return false;
}
return markdownSegments.some(segment => path.includes(segment));
}
// ================
// #region DOWNLOAD
// ================
function isCodePage() {
return /^\/([a-z]{2}(-[A-Z]{2})?\/)?scripts\/\d+-.+\/code/.test(window.location.pathname);
}
function initializeDownloadButton() {
const waitFor = (sel) =>
new Promise((resolve) => {
const el = document.querySelector(sel);
if (el) return resolve(el);
const obs = new MutationObserver(() => {
const el = document.querySelector(sel);
if (el) {
obs.disconnect();
resolve(el);
}
});
obs.observe(document, { childList: true, subtree: true });
});
waitFor('label[for="wrap-lines"]').then((label) => {
const wrapLinesCheckbox = document.getElementById('wrap-lines');
if (wrapLinesCheckbox) {
wrapLinesCheckbox.checked = false;
}
const toolbar = label.parentElement;
const btn = document.createElement('button');
btn.className = 'btn';
btn.textContent = getTranslation('download');
btn.style.marginLeft = '12px';
btn.style.backgroundColor = '#005200';
btn.style.color = 'white';
btn.style.border = 'none';
btn.style.padding = '6px 16px';
btn.style.borderRadius = '4px';
btn.style.cursor = 'pointer';
btn.addEventListener('mouseenter', () => btn.style.backgroundColor = '#1e971e');
btn.addEventListener('mouseleave', () => btn.style.backgroundColor = '#005200');
btn.addEventListener('click', () => {
const normalizedPath = normalizeScriptPath(window.location.pathname);
const scriptId = extractScriptIdFromNormalizedPath(normalizedPath);
if (!scriptId) {
alert(getTranslation('scriptIdNotFound'));
return;
}
const scriptUrl = `https://update.greasyfork.org/scripts/${scriptId}.js`;
btn.disabled = true;
btn.textContent = getTranslation('downloading');
GM_xmlhttpRequest({
method: 'GET',
url: scriptUrl,
onload: function (res) {
const code = res.responseText;
if (!code) {
alert(getTranslation('notFound'));
return;
}
const nameMatch = code.match(/\/\/\s*@name\s+(.+)/i);
const fileName = nameMatch ? `${nameMatch[1].trim()}.user.js` : 'script.user.js';
const blob = new Blob([code], { type: 'application/javascript;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
onerror: function (res) {
alert(getTranslation('downloadError'));
},
ontimeout: function () {
alert(getTranslation('downloadTimeout'));
},
onloadend: function () {
btn.disabled = false;
btn.textContent = getTranslation('download');
}
});
});
toolbar.appendChild(btn);
const spacer = document.createElement('div');
spacer.style.height = '12px';
toolbar.appendChild(spacer);
});
}
// ================
// #region INICIALIZAR
// ================
async function start() {
iconCache = await GM_getValue(CACHE_KEY, {});
await determineLanguage();
languageModal = createLanguageModal();
document.body.appendChild(languageModal);
registerLanguageMenu();
registerForceUpdateMenu();
setupThemeChangeListener();
if (isMarkdownPage()) {
applyToAllTextareas();
enableSourceEditorCheckbox();
}
if (isCodePage()){
initializeDownloadButton();
}
processIconElements();
highlightScriptDescription();
if (isScriptPage()) {
addAdditionalInfoSeparator();
}
makeDiscussionClickable();
applySyntaxHighlighting();
const observer = new MutationObserver(() => {
processIconElements();
highlightScriptDescription();
if (isScriptPage()) {
addAdditionalInfoSeparator();
}
if (isMarkdownPage()) {
applyToAllTextareas();
}
makeDiscussionClickable();
applySyntaxHighlighting();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
start();
})();