Greasy Fork

Greasy Fork is available in English.

更好的 Greasy Fork

为 Greasy Fork 增强多项实用功能:在标题旁显示脚本图标,在文本编辑器(用于评论和描述)中加入 Markdown 格式化工具,并在“代码”页面新增下载按钮,可将脚本直接下载为“.user.js”文件。此外,通过元数据为作者提供新的自定义选项,丰富脚本页面,显示高亮颜色、版权信息和社交图标。

当前为 2025-10-19 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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               http://greasyfork.icu/*
// @icon                https://gist.githubusercontent.com/0H4S/ff8b21d291d9cd8cdcf4cf1a0f96748c/raw/icon.svg
// @require             https://update.greasyfork.icu/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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    }

    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: /^(&lt;\/?|\/&gt;|&gt;)/ },
            { type: 'tag-name',         regex: /^([\w-]+)/, context: (t) => { const l=t[t.length-1]; return l&&l.type==='tag-punctuation'&&l.content.startsWith('&lt;') }},
            { 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('&gt;'))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.icu/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, `![alt text](${url})`); } },
            { 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.icu/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();
})();