향상된 Greasy Fork

Greasy Fork에 여러 유용한 기능을 추가합니다: 제목 옆에 스크립트 아이콘을 표시하고, 텍스트 편집기(댓글 및 설명용)에 Markdown 서식 도구를 추가하며, '코드' 페이지에 스크립트를 '.user.js' 파일로 직접 다운로드할 수 있는 새 다운로드 버튼을 만듭니다. 또한 메타데이터를 통해 저자를 위한 맞춤 설정 옵션을 제공해 강조 색상, 저작권 정보, 소셜 아이콘을 표시합니다.

// ==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, "&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.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, `![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.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();
})();