您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为 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, "&").replace(/</g, "<").replace(/>/g, ">"); } function highlight(code) { const keywords = new Set(['const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'of', 'in', 'async', 'await', 'try', 'catch', 'new', 'import', 'export', 'from', 'class', 'extends', 'super', 'true', 'false', 'null', 'undefined', 'document', 'window']); const tokens = []; let cursor = 0; const tokenDefinitions = [ { type: 'url', regex: /^(https?:\/\/[^\s"'`<>]+)/ }, { type: 'comment-special', regex: /^(\/\/[^\r\n]*)/ }, { type: 'comment', regex: /^(\/\*[\s\S]*?\*\/|<!--[\s\S]*?-->)/ }, { type: 'string', regex: /^(`(?:\\.|[^`])*`|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')/ }, { type: 'tag-punctuation', regex: /^(<\/?|\/>|>)/ }, { type: 'tag-name', regex: /^([\w-]+)/, context: (t) => { const l=t[t.length-1]; return l&&l.type==='tag-punctuation'&&l.content.startsWith('<') }}, { type: 'attribute', regex: /^([\w-]+)/, context: (t) => { for(let i=t.length-1;i>=0;i--){const n=t[i];if(n.type==='tag-punctuation'&&n.content.includes('>'))return!1;if(n.type==='tag-name')return!0;if(n.type==='whitespace')continue}return!1 }}, { type: 'regex', regex: /^(\/(?!\*)(?:[^\r\n/\\]|\\.)+\/[gimyus]*)/ }, { type: 'number', regex: /^\b-?(\d+(\.\d+)?)\b/ }, { type: 'keyword', regex: new RegExp(`^\\b(${Array.from(keywords).join('|')})\\b`) }, { type: 'function', regex: /^([a-zA-Z_][\w_]*)(?=\s*\()/ }, { type: 'property', regex: /^\.([a-zA-Z_][\w_]*)/ }, { type: 'operator', regex: /^(==?=?|!=?=?|=>|[+\-*/%&|^<>]=?|\?|:|=)/ }, { type: 'punctuation', regex: /^([,;(){}[\]])/ }, { type: 'whitespace', regex: /^\s+/ }, { type: 'unknown', regex: /^./ }, ]; let processedCode = escapeHtml(code); while (cursor < processedCode.length) { let matched = false; for (const def of tokenDefinitions) { if (def.context && !def.context(tokens)) { continue; } const match = def.regex.exec(processedCode.slice(cursor)); if (match) { const content = match[0]; if (def.type === 'function' && keywords.has(content)) { continue; } tokens.push({ type: def.type, content }); cursor += content.length; matched = true; break; } } if (!matched) { tokens.push({ type: 'unknown', content: processedCode[cursor] }); cursor++; } } for (let i = 0; i < tokens.length; i++) { if (tokens[i].type === 'string') { let nextToken = null; for(let j=i+1;j<tokens.length;j++){if(tokens[j].type!=='whitespace'){nextToken=tokens[j];break}} if (nextToken && nextToken.content === ':') { tokens[i].type = 'json-key'; } } } return tokens.map(token => { if (['whitespace', 'unknown', 'url'].includes(token.type)) return token.content; if (token.type === 'property') return `<span class="sh-punctuation">.</span><span class="sh-property">${token.content.slice(1)}</span>`; return `<span class="sh-${token.type}">${token.content}</span>`; }).join(''); } // ================ // #region ÍCONES // ================ let iconCache; const processedKeys = new Set(); async function saveCache() { await GM_setValue(CACHE_KEY, iconCache); } function normalizeScriptPath(pathname) { let withoutLocale = pathname.replace(/^\/[a-z]{2}(?:-[A-Z]{2})?\//, '/'); const match = withoutLocale.match(/^\/scripts\/\d+-.+?(?=\/|$)/); return match ? match[0] : null; } function extractScriptIdFromNormalizedPath(normalized) { const match = normalized.match(/\/scripts\/(\d+)-/); return match ? match[1] : null; } function createIconElement(src, isHeader = false) { const img = document.createElement('img'); img.src = src; img.alt = ''; if (isHeader) { img.style.cssText = ` width: 80px; height: 80px; margin-right: 10px; vertical-align: middle; border-radius: 4px; object-fit: contain; pointer-events: none; `; } else { img.style.cssText = ` width: 40px; height: 40px; margin-right: 8px; vertical-align: middle; border-radius: 3px; object-fit: contain; pointer-events: none; `; } img.loading = 'lazy'; return img; } function extractMetadataFromContent(content) { if (typeof content !== 'string') return {}; const metadata = {}; const lines = content.split('\n'); const supportedTags = new Set([ '@icon', '@bgf-colorLT', '@bgf-colorDT', '@bgf-compatible', '@bgf-copyright', '@bgf-social' ]); for (const line of lines) { const trimmedLine = line.trim(); if (trimmedLine.startsWith('// ==/UserScript==')) break; if (!trimmedLine.startsWith('// @')) continue; const match = trimmedLine.match(/\/\/\s*(@[a-zA-Z0-9-]+)\s+(.+)/); if (!match) continue; const key = match[1]; let value = match[2].trim(); if (supportedTags.has(key) && !metadata.hasOwnProperty(key)) { if (key === '@bgf-colorLT' || key === '@bgf-colorDT') { const colorRegex = /(#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\s*\([^)]+\))/; const colorMatch = value.match(colorRegex); if (colorMatch) { value = colorMatch[0]; } else { value = value.split(',')[0].trim(); } } metadata[key] = value; } } return metadata; } function isValidIconUrl(url) { return url && (url.startsWith('http') || url.startsWith('')); } async function processScript(normalizedPath, targetElement, isHeader = false) { if (processedKeys.has(normalizedPath) && isHeader) { applyBfgFeatures(iconCache[normalizedPath]); } if (processedKeys.has(normalizedPath) && !isHeader) { const cached = iconCache[normalizedPath]; if (cached && isValidIconUrl(cached.iconUrl)) { targetElement.prepend(createIconElement(cached.iconUrl, isHeader)); } return; } processedKeys.add(normalizedPath); const cached = iconCache[normalizedPath]; const now = Date.now(); const applyColorToBlockquote = (metadata) => { const blockquotes = document.querySelectorAll(`blockquote.script-description-blockquote[data-bgf-path="${normalizedPath}"]`); if (blockquotes.length === 0) return; const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; const colorToApply = isDarkMode ? metadata.bgfColorDT : metadata.bgfColorLT; blockquotes.forEach(bq => { if (colorToApply) { bq.style.setProperty('border-left-color', colorToApply, 'important'); } else { bq.style.removeProperty('border-left-color'); } }); }; if (cached && now - cached.ts < 30 * 24 * 60 * 60 * 1000) { if (isValidIconUrl(cached.iconUrl)) { targetElement.prepend(createIconElement(cached.iconUrl, isHeader)); } applyColorToBlockquote(cached); if (isHeader) { applyBfgFeatures(cached); } return; } const scriptId = extractScriptIdFromNormalizedPath(normalizedPath); if (!scriptId) { iconCache[normalizedPath] = { ts: now }; await saveCache(); return; } const scriptUrl = `https://update.greasyfork.org/scripts/${scriptId}.js`; GM_xmlhttpRequest({ method: 'GET', url: scriptUrl, timeout: 6000, onload: async function (res) { if (typeof res.responseText !== 'string') { iconCache[normalizedPath] = { ts: now }; await saveCache(); return; } const rawMetadata = extractMetadataFromContent(res.responseText); const metadata = { iconUrl: rawMetadata['@icon'] || null, bgfColorLT: rawMetadata['@bgf-colorLT'] || null, bgfColorDT: rawMetadata['@bgf-colorDT'] || null, bgfCompatible: rawMetadata['@bgf-compatible'] || null, bgfCopyright: rawMetadata['@bgf-copyright'] || null, bgfSocial: rawMetadata['@bgf-social'] || null, ts: now }; iconCache[normalizedPath] = metadata; await saveCache(); if (isValidIconUrl(metadata.iconUrl)) { targetElement.prepend(createIconElement(metadata.iconUrl, isHeader)); } applyColorToBlockquote(metadata); if (isHeader) { applyBfgFeatures(metadata); } }, onerror: async function () { iconCache[normalizedPath] = { ts: now }; await saveCache(); } }); } function handleScriptLink(linkEl) { if (linkEl._handled) return; linkEl._handled = true; const href = linkEl.getAttribute('href'); if (!href || !href.startsWith('/')) return; try { const url = new URL(href, window.location.origin); const normalized = normalizeScriptPath(url.pathname); if (!normalized) return; setTimeout(() => processScript(normalized, linkEl, false), 0); } catch (e) {} } function handleMainHeaderH2() { const headers = document.querySelectorAll('header'); for (const header of headers) { const h2 = header.querySelector('h2'); const desc = header.querySelector('p.script-description'); if (h2 && desc && !h2._handled) { h2._handled = true; const normalized = normalizeScriptPath(window.location.pathname); if (!normalized) return; setTimeout(() => processScript(normalized, h2, true), 0); break; } } } function processIconElements() { document.querySelectorAll('a.script-link:not([data-icon-processed])') .forEach(el => { el.setAttribute('data-icon-processed', '1'); handleScriptLink(el); }); handleMainHeaderH2(); } // ================ // #region RECURSOS BFG // ================ function applyBfgFeatures(metadata) { if (!metadata) return; applyBfgCompatibility(metadata.bgfCompatible); applyBfgCopyright(metadata.bgfCopyright); applyBfgSocial(metadata.bgfSocial); } function applyBfgCompatibility(compatValue) { if (!compatValue) return; const compatDd = document.querySelector('dd.script-show-compatibility'); if (!compatDd) { return; } let compatContainer = compatDd.querySelector('span'); if (!compatContainer) { compatContainer = document.createElement('span'); compatDd.innerHTML = ''; compatDd.appendChild(compatContainer); } const compatItems = compatValue.split(',').map(item => item.trim().toLowerCase()); compatItems.forEach(item => { if (!icons[item] || compatContainer.querySelector(`.bgf-compat-${item}`)) { return; } const img = document.createElement('img'); img.className = `browser-compatible bgf-compat-${item}`; const displayName = capitalizeCompatItem(item); img.alt = `${getTranslation('compatible_with')} ${displayName}`; img.title = `${getTranslation('compatible_with')} ${displayName}`; img.style.marginLeft = '1px'; img.src = `data:image/svg+xml;utf8,${encodeURIComponent(icons[item])}`; compatContainer.appendChild(img); }); } function reapplyAllBlockquoteColors() { const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches; const allBlockquotes = document.querySelectorAll('blockquote.script-description-blockquote[data-bgf-path]'); allBlockquotes.forEach(bq => { const path = bq.dataset.bgfPath; if (!path || !iconCache[path]) return; const metadata = iconCache[path]; const colorToApply = isDarkMode ? metadata.bgfColorDT : metadata.bgfColorLT; if (colorToApply) { bq.style.setProperty('border-left-color', colorToApply, 'important'); } else { bq.style.removeProperty('border-left-color'); } }); } function setupThemeChangeListener() { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', reapplyAllBlockquoteColors); } function applyBfgCopyright(copyrightValue) { if (!copyrightValue || document.querySelector('.script-show-copyright')) return; const copyrightRegex = /\[(.{1,50})\]\((https:\/\/gist\.github\.com\/[^)]+)\)/; const match = copyrightValue.match(copyrightRegex); if (!match) return; const licenseDd = document.querySelector('dd.script-show-license'); if (!licenseDd) return; const text = match[1]; const url = match[2]; const copyrightDt = document.createElement('dt'); copyrightDt.className = 'script-show-copyright'; copyrightDt.innerHTML = '<span>Copyright</span>'; const copyrightDd = document.createElement('dd'); copyrightDd.className = 'script-show-copyright'; copyrightDd.style.alignSelf = 'center'; const link = document.createElement('a'); link.href = url; link.textContent = text; link.target = '_blank'; link.rel = 'noopener noreferrer'; const span = document.createElement('span'); span.appendChild(link); copyrightDd.appendChild(span); licenseDd.after(copyrightDt, copyrightDd); } function applyBfgSocial(socialValue) { if (!socialValue || document.querySelector('.script-show-social')) return; const authorDd = document.querySelector('dd.script-show-author'); if (!authorDd) return; const socialDomainMap = { 'instagram.com': { icon: icons.instagram, name: 'Instagram' }, 'facebook.com': { icon: icons.facebook, name: 'Facebook' }, 'x.com': { icon: icons.x, name: 'X / Twitter' }, 'youtube.com': { icon: icons.youtube, name: 'YouTube' }, 'bilibili.com': { icon: icons.bilibili, name: 'Bilibili' }, 'tiktok.com': { icon: icons.tiktok, name: 'TikTok' }, 'douyin.com': { icon: icons.tiktok, name: 'Douyin' }, 'github.com': { icon: icons.github, name: 'GitHub' }, 'linkedin.com': { icon: icons.linkedin, name: 'LinkedIn' }, }; const urls = socialValue.split(',').map(url => url.trim()); const validLinks = []; let tiktokFamilyProcessed = false; urls.forEach(url => { try { const domain = new URL(url).hostname.replace('www.', ''); if (socialDomainMap[domain]) { if (domain === 'tiktok.com' || domain === 'douyin.com') { if (tiktokFamilyProcessed) return; tiktokFamilyProcessed = true; } validLinks.push({ url, ...socialDomainMap[domain] }); } } catch (e) {} }); if (validLinks.length === 0) return; const socialDt = document.createElement('dt'); socialDt.className = 'script-show-social'; socialDt.innerHTML = '<span>Social</span>'; const socialDd = document.createElement('dd'); socialDd.className = 'script-show-social'; socialDd.style.cssText = 'display: flex; gap: 8px; align-items: center; align-self: center;'; validLinks.forEach(linkInfo => { const link = document.createElement('a'); link.href = linkInfo.url; link.title = linkInfo.name; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.innerHTML = linkInfo.icon; const svg = link.querySelector('svg'); if (svg) { svg.style.width = '20px'; svg.style.height = '20px'; svg.style.verticalAlign = 'middle'; } socialDd.appendChild(link); }); authorDd.after(socialDt, socialDd); } // ================ // #region EDITOR MD // ================ function insertText(textarea, prefix, suffix = '', placeholder = '') { const start = textarea.selectionStart; const end = textarea.selectionEnd; const selected = textarea.value.substring(start, end); const text = selected || placeholder; textarea.setRangeText(prefix + text + suffix, start, end, selected ? 'end' : 'select'); textarea.focus(); } function createToolbarButton(def) { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'txt-editor-toolbar-button'; btn.dataset.tooltip = def.title; btn.innerHTML = def.icon || def.label; btn.addEventListener('click', e => { e.preventDefault(); def.action(); }); return btn; } function createTextStyleEditor(textarea) { if (textarea.dataset.editorApplied) return; textarea.dataset.editorApplied = 'true'; const container = document.createElement('div'); container.className = 'txt-editor-container'; const toolbar = document.createElement('div'); toolbar.className = 'txt-editor-toolbar'; const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); function applyTheme(isDark) { container.classList.toggle('dark-theme', isDark); container.classList.toggle('light-theme', !isDark); } applyTheme(mediaQuery.matches); mediaQuery.addEventListener('change', e => applyTheme(e.matches)); const tools = [ { type: 'select', title: getTranslation('titles'), options: { 'H1': '# ', 'H2': '## ', 'H3': '### ', 'H4': '#### ', 'H5': '##### ', 'H6': '###### ' }, action: (val) => insertText(textarea, val, '', getTranslation('title_placeholder')) }, { type: 'divider' }, { title: getTranslation('bold'), icon: icons.bold, action: () => insertText(textarea, '**', '**', getTranslation('bold_placeholder')) }, { title: getTranslation('italic'), icon: icons.italic, action: () => insertText(textarea, '*', '*', getTranslation('italic_placeholder')) }, { title: getTranslation('underline'), icon: icons.underline, action: () => insertText(textarea, '<u>', '</u>', getTranslation('underline_placeholder')) }, { title: getTranslation('strikethrough'), icon: icons.strikethrough, action: () => insertText(textarea, '~~', '~~', getTranslation('strikethrough_placeholder')) }, { type: 'divider' }, { title: getTranslation('unordered_list'), icon: icons.ul, action: () => { const start = textarea.selectionStart, end = textarea.selectionEnd, selection = textarea.value.substring(start, end); textarea.setRangeText(selection ? selection.split('\n').map(line => line.trim() === '' ? '' : '- ' + line).join('\n') : '\n- ' + getTranslation('list_item_placeholder'), start, end, 'select'); textarea.focus(); } }, { title: getTranslation('ordered_list'), icon: icons.ol, action: () => { const start = textarea.selectionStart, end = textarea.selectionEnd, selection = textarea.value.substring(start, end); if (selection) { let counter = 1; textarea.setRangeText(selection.split('\n').map(line => line.trim() === '' ? '' : (counter++) + '. ' + line).join('\n'), start, end, 'select'); } else insertText(textarea, '\n1. ', '', getTranslation('list_item_placeholder')); textarea.focus(); } }, { type: 'divider' }, { title: getTranslation('quote'), icon: icons.quote, action: () => { const start = textarea.selectionStart, end = textarea.selectionEnd, selection = textarea.value.substring(start, end); textarea.setRangeText(selection ? selection.split('\n').map(line => line.trim() === '' ? '' : '> ' + line).join('\n') : '\n> ' + getTranslation('quote_placeholder'), start, end, 'select'); textarea.focus(); } }, { title: getTranslation('inline_code'), icon: icons.code, action: () => insertText(textarea, '`', '`', getTranslation('inline_code_placeholder')) }, { title: getTranslation('code_block'), label: icons.code_block, action: () => insertText(textarea, '\n```\n', '\n```\n', getTranslation('code_block_placeholder')) }, { title: getTranslation('horizontal_line'), icon: icons.hr, action: () => insertText(textarea, '\n---\n') }, { type: 'divider' }, { title: getTranslation('link'), icon: icons.link, action: () => { const url = prompt(getTranslation('prompt_insert_url'), "https://"); if (url) insertText(textarea, '[', `](${url})`, getTranslation('link_text_placeholder')); } }, { title: getTranslation('image'), icon: icons.image, action: () => { const url = prompt(getTranslation('prompt_insert_image_url'), "https://"); if (url) insertText(textarea, ``); } }, { title: getTranslation('table'), icon: icons.table, action: () => { const cols = parseInt(prompt(getTranslation('prompt_columns'), "3"), 10) || 3; const rows = parseInt(prompt(getTranslation('prompt_rows'), "2"), 10) || 2; let table = '\n| ' + Array(cols).fill(getTranslation('table_header_placeholder')).join(' | ') + ' |\n'; table += '| ' + Array(cols).fill('---').join(' | ') + ' |\n'; for (let i = 0; i < rows; i++) { table += '| ' + Array(cols).fill(getTranslation('table_cell_placeholder')).join(' | ') + ' |\n'; } insertText(textarea, table); } }, { title: getTranslation('video'), icon: icons.video, action: () => { const url = prompt(getTranslation('prompt_insert_video_url')); if (!url) return; let src = ''; if (url.includes('youtube.com/watch?v=')) src = `https://www.youtube.com/embed/${new URL(url).searchParams.get('v')}`; else if (url.includes('youtu.be/')) src = `https://www.youtube.com/embed/${new URL(url).pathname.substring(1)}`; else if (url.includes('bilibili.com/video/')) src = `https://player.bilibili.com/player.html?bvid=${new URL(url).pathname.split('/')[2]}`; if (src) insertText(textarea, `<iframe src="${src}" allowfullscreen></iframe>`); else alert(getTranslation('alert_invalid_video_url')); } }, { type: 'divider' }, { title: getTranslation('subscript'), label: icons.subscript, action: () => insertText(textarea, '<sub>', '</sub>', getTranslation('subscript_placeholder')) }, { title: getTranslation('superscript'), label: icons.superscript, action: () => insertText(textarea, '<sup>', '</sup>', getTranslation('superscript_placeholder')) }, { title: getTranslation('highlight'), label: icons.highlight, action: () => insertText(textarea, '<mark>', '</mark>', getTranslation('highlight_placeholder')) }, { title: getTranslation('keyboard'), label: icons.keyboard, action: () => insertText(textarea, '<kbd>', '</kbd>', getTranslation('keyboard_placeholder')) }, { title: getTranslation('abbreviation'), label: icons.abbreviation, action: () => { const title = prompt(getTranslation('prompt_abbreviation_meaning')); if (title) insertText(textarea, `<abbr title="${title}">`, `</abbr>`, getTranslation('abbreviation_placeholder')); } }, { type: 'color-picker' } ]; tools.forEach(tool => { if (tool.type === 'divider') { const div = document.createElement('div'); div.className = 'txt-editor-toolbar-divider'; toolbar.appendChild(div); } else if (tool.type === 'select') { const container = document.createElement('span'); container.className = 'txt-editor-toolbar-button'; container.dataset.tooltip = tool.title; container.style.position = 'relative'; container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.justifyContent = 'center'; container.innerHTML = icons.h; const select = document.createElement('select'); select.className = 'txt-editor-toolbar-select'; select.style.cssText = ` -webkit-appearance: none; appearance: none; background: transparent; border: none; color: transparent; position: absolute; top: 0; left: 0; width: 100%; height: 100%; cursor: pointer; `; const placeholderOpt = document.createElement('option'); placeholderOpt.value = ''; placeholderOpt.textContent = ''; placeholderOpt.disabled = true; placeholderOpt.selected = true; placeholderOpt.style.display = 'none'; select.appendChild(placeholderOpt); Object.keys(tool.options).forEach(key => { const opt = document.createElement('option'); opt.value = tool.options[key]; opt.textContent = key; select.appendChild(opt); }); select.addEventListener('change', () => { if (select.value) tool.action(select.value); select.selectedIndex = 0; }); container.appendChild(select); toolbar.appendChild(container); } else if (tool.type === 'color-picker') { const colorContainer = document.createElement('div'); colorContainer.className = 'txt-color-picker-container'; const input = document.createElement('input'); input.type = 'color'; input.className = 'txt-color-picker-input'; input.value = "#58a6ff"; const colorBtn = createToolbarButton({ title: getTranslation('text_color'), label: icons.text_color, action: () => insertText(textarea, `<span style="color: ${input.value};">`, '</span>', getTranslation('colored_text_placeholder')) }); const bgBtn = createToolbarButton({ title: getTranslation('background_color'), label: icons.background_color, action: () => insertText(textarea, `<span style="background-color: ${input.value};">`, '</span>', getTranslation('colored_background_placeholder')) }); colorContainer.append(input, colorBtn, bgBtn); toolbar.appendChild(colorContainer); } else { toolbar.appendChild(createToolbarButton(tool)); } }); textarea.parentNode.insertBefore(container, textarea); container.append(toolbar, textarea); } function applyToAllTextareas() { const textareas = document.querySelectorAll('textarea:not(#script_version_code):not([data-editor-applied])'); textareas.forEach(createTextStyleEditor); } function enableSourceEditorCheckbox() { const enableCheckbox = () => { const checkbox = document.getElementById('enable-source-editor-code'); if (checkbox && !checkbox.checked) { checkbox.checked = true; const event = new Event('change', { bubbles: true }); checkbox.dispatchEvent(event); } }; enableCheckbox(); const observer = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.type === 'childList') { const checkbox = document.getElementById('enable-source-editor-code'); if (checkbox) { enableCheckbox(); observer.disconnect(); break; } } } }); observer.observe(document.body, { childList: true, subtree: true }); } function isMarkdownPage() { const path = window.location.pathname; const markdownSegments = [ '/new', '/edit', '/feedback', '/discussions' ]; if (path.includes('/sets/')) { return false; } return markdownSegments.some(segment => path.includes(segment)); } // ================ // #region DOWNLOAD // ================ function isCodePage() { return /^\/([a-z]{2}(-[A-Z]{2})?\/)?scripts\/\d+-.+\/code/.test(window.location.pathname); } function initializeDownloadButton() { const waitFor = (sel) => new Promise((resolve) => { const el = document.querySelector(sel); if (el) return resolve(el); const obs = new MutationObserver(() => { const el = document.querySelector(sel); if (el) { obs.disconnect(); resolve(el); } }); obs.observe(document, { childList: true, subtree: true }); }); waitFor('label[for="wrap-lines"]').then((label) => { const wrapLinesCheckbox = document.getElementById('wrap-lines'); if (wrapLinesCheckbox) { wrapLinesCheckbox.checked = false; } const toolbar = label.parentElement; const btn = document.createElement('button'); btn.className = 'btn'; btn.textContent = getTranslation('download'); btn.style.marginLeft = '12px'; btn.style.backgroundColor = '#005200'; btn.style.color = 'white'; btn.style.border = 'none'; btn.style.padding = '6px 16px'; btn.style.borderRadius = '4px'; btn.style.cursor = 'pointer'; btn.addEventListener('mouseenter', () => btn.style.backgroundColor = '#1e971e'); btn.addEventListener('mouseleave', () => btn.style.backgroundColor = '#005200'); btn.addEventListener('click', () => { const normalizedPath = normalizeScriptPath(window.location.pathname); const scriptId = extractScriptIdFromNormalizedPath(normalizedPath); if (!scriptId) { alert(getTranslation('scriptIdNotFound')); return; } const scriptUrl = `https://update.greasyfork.org/scripts/${scriptId}.js`; btn.disabled = true; btn.textContent = getTranslation('downloading'); GM_xmlhttpRequest({ method: 'GET', url: scriptUrl, onload: function (res) { const code = res.responseText; if (!code) { alert(getTranslation('notFound')); return; } const nameMatch = code.match(/\/\/\s*@name\s+(.+)/i); const fileName = nameMatch ? `${nameMatch[1].trim()}.user.js` : 'script.user.js'; const blob = new Blob([code], { type: 'application/javascript;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }, onerror: function (res) { alert(getTranslation('downloadError')); }, ontimeout: function () { alert(getTranslation('downloadTimeout')); }, onloadend: function () { btn.disabled = false; btn.textContent = getTranslation('download'); } }); }); toolbar.appendChild(btn); const spacer = document.createElement('div'); spacer.style.height = '12px'; toolbar.appendChild(spacer); }); } // ================ // #region INICIALIZAR // ================ async function start() { iconCache = await GM_getValue(CACHE_KEY, {}); await determineLanguage(); languageModal = createLanguageModal(); document.body.appendChild(languageModal); registerLanguageMenu(); registerForceUpdateMenu(); setupThemeChangeListener(); if (isMarkdownPage()) { applyToAllTextareas(); enableSourceEditorCheckbox(); } if (isCodePage()){ initializeDownloadButton(); } processIconElements(); highlightScriptDescription(); if (isScriptPage()) { addAdditionalInfoSeparator(); } makeDiscussionClickable(); applySyntaxHighlighting(); const observer = new MutationObserver(() => { processIconElements(); highlightScriptDescription(); if (isScriptPage()) { addAdditionalInfoSeparator(); } if (isMarkdownPage()) { applyToAllTextareas(); } makeDiscussionClickable(); applySyntaxHighlighting(); }); observer.observe(document.body, { childList: true, subtree: true }); } start(); })();