通过多项改进使Greasy Fork更加实用且美观:在标题旁添加脚本图标;实现用于编辑描述和评论的完整HTML工具栏;创建新的脚本直接下载按钮;允许通过元数据进行自定义(颜色、版权、社交图标);以及其他界面和可用性优化。
当前为
// ==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.7
// @description Makes Greasy Fork more functional and visually appealing through various improvements: adds script icons next to titles; implements a complete HTML toolbar for editing descriptions and comments; creates a new direct script download button; allows customizations via metadata (colors, copyright, social icons); plus other interface and usability optimizations.
// @description:pt-BR Torna o Greasy Fork mais funcional e visualmente agradável através de várias melhorias: adiciona ícones de scripts ao lado dos títulos; implementa uma barra de ferramentas HTML completa para editar descrições e comentários; cria um novo botão de download direto de scripts; permite personalizações via metadados (cores, copyright, ícones sociais); além de outras otimizações de interface e usabilidade.
// @description:zh-CN 通过多项改进使Greasy Fork更加实用且美观:在标题旁添加脚本图标;实现用于编辑描述和评论的完整HTML工具栏;创建新的脚本直接下载按钮;允许通过元数据进行自定义(颜色、版权、社交图标);以及其他界面和可用性优化。
// @description:zh-TW 透過多項改進使Greasy Fork更加實用且美觀:在標題旁新增腳本圖示;實作用於編輯描述和留言的完整HTML工具列;建立新的腳本直接下載按鈕;允許透過中繼資料進行自訂(顏色、版權、社群圖示);以及其他介面和可用性最佳化。
// @description:en Makes Greasy Fork more functional and visually appealing through various improvements: adds script icons next to titles; implements a complete HTML toolbar for editing descriptions and comments; creates a new direct script download button; allows customizations via metadata (colors, copyright, social icons); plus other interface and usability optimizations.
// @description:es Hace que Greasy Fork sea más funcional y visualmente atractivo mediante diversas mejoras: añade iconos de scripts junto a los títulos; implementa una barra de herramientas HTML completa para editar descripciones y comentarios; crea un nuevo botón de descarga directa de scripts; permite personalizaciones mediante metadatos (colores, copyright, iconos sociales); además de otras optimizaciones de interfaz y usabilidad.
// @description:ja 複数の改善によりGreasy Forkをより機能的で視覚的に魅力的にします:タイトルの横にスクリプトアイコンを追加、説明文やコメントを編集するための完全なHTMLツールバーを実装、スクリプトの新しい直接ダウンロードボタンを作成、メタデータによるカスタマイズ(色、著作権、ソーシャルアイコン)を可能にし、その他のインターフェースとユーザビリティの最適化も提供します。
// @description:ko 다양한 개선을 통해 Greasy Fork를 더욱 기능적이고 시각적으로 매력적으로 만듭니다: 제목 옆에 스크립트 아이콘 추가, 설명 및 댓글 편집을 위한 완전한 HTML 도구 모음 구현, 새로운 스크립트 직접 다운로드 버튼 생성, 메타데이터를 통한 사용자 지정(색상, 저작권, 소셜 아이콘) 허용, 기타 인터페이스 및 사용성 최적화 제공.
// @description:de Macht Greasy Fork funktionaler und optisch ansprechender durch verschiedene Verbesserungen: fügt Script-Icons neben Titeln hinzu; implementiert eine vollständige HTML-Symbolleiste zum Bearbeiten von Beschreibungen und Kommentaren; erstellt eine neue Schaltfläche für direkten Script-Download; ermöglicht Anpassungen über Metadaten (Farben, Urheberrecht, Social-Icons); sowie weitere Interface- und Usability-Optimierungen.
// @description:fr Rend Greasy Fork plus fonctionnel et visuellement agréable grâce à diverses améliorations : ajoute des icônes de scripts à côté des titres ; implémente une barre d'outils HTML complète pour éditer les descriptions et commentaires ; crée un nouveau bouton de téléchargement direct de scripts ; permet des personnalisations via les métadonnées (couleurs, copyright, icônes sociales) ; ainsi que d'autres optimisations d'interface et d'utilisabilité.
// @author OHAS
// @license CC-BY-NC-ND-4.0
// @copyright 2025 OHAS. All Rights Reserved.
// @match https://greasyfork.org/*
// @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjQgMjQiIG92ZXJmbG93PSJ2aXNpYmxlIj4KICAgIDxzdHlsZT4KICAgICAgICAuZGVzZW5oYXItY29udG9ybm8gewogICAgICAgICAgICBzdHJva2UtZGFzaGFycmF5OiAxMDA7CiAgICAgICAgICAgIHN0cm9rZS1kYXNob2Zmc2V0OiAxMDA7CiAgICAgICAgICAgIGFuaW1hdGlvbjogZGVzZW5oYXItY29udG9ybm8gMTBzIGVhc2UtaW4tb3V0IGluZmluaXRlOwogICAgICAgIH0KCiAgICAgICAgLm9uZGEtcHJlZW5jaGltZW50byB7CiAgICAgICAgICAgIGFuaW1hdGlvbjogb25kYS1wcmVlbmNoaW1lbnRvIDEwcyBlYXNlLWluLW91dCBpbmZpbml0ZTsKICAgICAgICB9CgogICAgICAgIEBrZXlmcmFtZXMgZGVzZW5oYXItY29udG9ybm8gewogICAgICAgICAgICAwJSB7CiAgICAgICAgICAgICAgICBzdHJva2UtZGFzaG9mZnNldDogMTAwOwogICAgICAgICAgICAgICAgc3Ryb2tlOiAjZmZmZmZmZmY7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgMjUlIHsKICAgICAgICAgICAgICAgIHN0cm9rZS1kYXNob2Zmc2V0OiAwOwogICAgICAgICAgICAgICAgc3Ryb2tlOiAjZmZmZmZmZmY7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgNzUlIHsKICAgICAgICAgICAgICAgIHN0cm9rZS1kYXNob2Zmc2V0OiAwOwogICAgICAgICAgICAgICAgc3Ryb2tlOiAjZmZmZmZmZmY7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgMTAwJSB7CiAgICAgICAgICAgICAgICBzdHJva2UtZGFzaG9mZnNldDogLTEwMDsKICAgICAgICAgICAgICAgIHN0cm9rZTogI2ZmZmZmZmZmOwogICAgICAgICAgICB9CiAgICAgICAgfQoKICAgICAgICBAa2V5ZnJhbWVzIG9uZGEtcHJlZW5jaGltZW50byB7CiAgICAgICAgICAgIDAlIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlWSgyNHB4KTsKICAgICAgICAgICAgICAgIGZpbGw6ICNmZmZmZmZmZjsKICAgICAgICAgICAgfQogICAgICAgICAgICAyNSUgewogICAgICAgICAgICAgICAgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDBweCk7CiAgICAgICAgICAgICAgICBmaWxsOiAjZmZmZmZmZmY7CiAgICAgICAgICAgIH0KICAgICAgICAgICAgNzUlIHsKICAgICAgICAgICAgICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlWSgwcHgpOwogICAgICAgICAgICAgICAgZmlsbDogI2ZmZmZmZmZmOwogICAgICAgICAgICB9CiAgICAgICAgICAgIDEwMCUgewogICAgICAgICAgICAgICAgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDI0cHgpOwogICAgICAgICAgICAgICAgZmlsbDogI2ZmZmZmZmZmOwogICAgICAgICAgICB9CiAgICAgICAgfQogICAgPC9zdHlsZT4KICAgIDxkZWZzPgogICAgICAgIDxjbGlwUGF0aCBpZD0ibW9sZGUtZ2FyZm8iPgogICAgICAgICAgICA8cGF0aCBkPSJNNS44OSAyLjIyN2EuMjguMjggMCAwIDEgLjI2Ni4wNzZsNS4wNjMgNS4wNjJjLjU0LjU0LjUwOSAxLjY1Mi0uMDMxIDIuMTkybDguNzcxIDguNzdjMS4zNTYgMS4zNTUtLjM2IDMuMDk3LTEuNzMgMS43MjhsLTguNzcyLTguNzdjLS41NC41NC0xLjY1MS41NzEtMi4xOTEuMDMxbC01LjA2My01LjA2Yy0uMzA0LS4zMDQuMzA0LS45MTEuNjA4LS42MDhsMy43MTQgMy43MTNMNy41OSA4LjI5N0wzLjg3NSA0LjU4MmMtLjMwNC0uMzA0LjMwNC0uOTExLjYwNy0uNjA3bDMuNzE1IDMuNzE0bDEuMDY3LTEuMDY2TDUuNTQ5IDIuOTFjLS4yMjgtLjIyOC4wNTctLjYyNi4zNDItLjY4M1oiLz4KICAgICAgICA8L2NsaXBQYXRoPgogICAgPC9kZWZzPgogICAgPGc+CiAgICAgICAgPHBhdGggZmlsbD0iIzAwMDAwMGZmIiBkPSJNMTIgMEM1LjM3NCAwIDAgNS4zNzUgMCAxMnM1LjM3NCAxMiAxMiAxMmM2LjYyNSAwIDEyLTUuMzc1IDEyLTEyUzE4LjYyNSAwIDEyIDAiLz4KICAgICAgICA8ZyBjbGlwLXBhdGg9InVybCgjbW9sZGUtZ2FyZm8pIj4KICAgICAgICAgICAgPHBhdGggY2xhc3M9Im9uZGEtcHJlZW5jaGltZW50byIgCiAgICAgICAgICAgICAgICAgIGQ9Ik0gLTIsMjQgTCAtMiwwIGMgNiwtNSA2LDUgMTIsMCBzIDYsLTUgMTIsMCBzIDYsLTUgMTIsMCBMIDMwLDI0IFoiLz4KICAgICAgICA8L2c+CiAgICAgICAgPHBhdGggY2xhc3M9ImRlc2VuaGFyLWNvbnRvcm5vIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjAuNSIgZD0iTTUuODkgMi4yMjdhLjI4LjI4IDAgMCAxIC4yNjYuMDc2bDUuMDYzIDUuMDYyYy41NC41NC41MDkgMS42NTItLjAzMSAyLjE5Mmw4Ljc3MSA4Ljc3YzEuMzU2IDEuMzU1LS4zNiAzLjA5Ny0xLjczIDEuNzI4bC04Ljc3Mi04Ljc3Yy0uNTQuNTQtMS42NTEuNTcxLTIuMTkxLjAzMWwtNS4wNjMtNS4wNmMtLjMwNC0uMzA0LjMwNC0uOTExLjYwOC0uNjA4bDMuNzE0IDMuNzEzTDcuNTkgOC4yOTdMMy44NzUgNC41ODJjLS4zMDQtLjMwNC4zMDQtLjkxMS42MDctLjYwN2wzLjcxNSAzLjcxNGwxLjA2Ny0xLjA2Nkw1LjU0OSAyLjkxYy0uMjI4LS4yMjguMDU3LS42MjYuMzQyLS42ODNaIi8+CiAgICA8L2c+Cjwvc3ZnPg==
// @resource customCSS https://cdn.jsdelivr.net/gh/0H4S/Better-Greasy-Fork/estilo.css
// @resource iconsJSON https://cdn.jsdelivr.net/gh/0H4S/Better-Greasy-Fork/icones.json
// @require https://update.greasyfork.org/scripts/549920.js
// @connect gist.github.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
// @noframes
// @compatible chrome
// @compatible firefox
// @compatible edge
// @compatible opera
// @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
// ==/UserScript==
(function () {
'use strict';
// ================
// #region GLOBAL
// ================
const allTranslations = {
"en": {
"langName": "English",
"languageSettings": "🌐 Language",
"close": "Close",
"confirm": "Confirm",
"cancel": "Cancel",
"download": "Download",
"compatible_with": "Compatible with",
"force_update": "🔄️ Force Update",
"force_update_alert": "Cache cleared. The page will reload to fetch the updated data.",
"titles": "Headings",
"title_placeholder": "Heading",
"bold": "Bold",
"bold_placeholder": "bold text",
"italic": "Italic",
"italic_placeholder": "italic text",
"underline": "Underline",
"underline_placeholder": "underlined text",
"strikethrough": "Strikethrough",
"strikethrough_placeholder": "strikethrough text",
"unordered_list": "Unordered List",
"ordered_list": "Ordered List",
"list_item_placeholder": "Item",
"quote": "Quote",
"inline_code": "Inline Code",
"inline_code_placeholder": "code",
"code_block": "Code Block",
"code_block_placeholder": "code here",
"horizontal_line": "Horizontal Line",
"horizontal_line_style": "Horizontal Line Style",
"prompt_hr_size": "Size (px)",
"prompt_hr_color": "Color",
"link": "Link",
"prompt_insert_url": "Enter the URL:",
"link_text_placeholder": "link text",
"image": "Image",
"prompt_insert_image_url": "Enter the image URL (https):",
"prompt_image_title": "Image title (optional):",
"image_title_placeholder": "e.g. My beautiful image",
"prompt_image_width": "Width (optional):",
"prompt_image_height": "Height (optional):",
"video": "Video",
"prompt_video_type": "Video Type",
"video_type_embed": "Embed (YouTube, Bilibili)",
"video_type_html5": "HTML5 Video (Direct URL)",
"prompt_video_poster_url": "Poster Image URL",
"prompt_insert_video_url": "Enter the video URL:",
"prompt_video_width": "Width (optional):",
"prompt_video_height": "Height (optional):",
"alert_invalid_video_url": "Invalid or unsupported video URL.",
"table": "Table",
"prompt_columns": "Number of columns:",
"prompt_rows": "Number of rows:",
"table_header_placeholder": "Header",
"table_cell_placeholder": "Cell",
"subscript": "Subscript",
"subscript_placeholder": "sub",
"superscript": "Superscript",
"superscript_placeholder": "sup",
"highlight": "Highlight",
"highlight_placeholder": "highlighted",
"keyboard": "Keyboard",
"keyboard_placeholder": "Ctrl+C",
"abbreviation": "Abbreviation",
"prompt_abbreviation_meaning": "What does the abbreviation stand for?",
"abbreviation_placeholder": "HTML",
"text_color": "Text Color",
"colored_text_placeholder": "colored text",
"background_color": "Background Color",
"colored_background_placeholder": "colored background",
"details": "Collapsible section",
"details_summary_placeholder": "Summary or Title",
"details_content_placeholder": "Content to be hidden...",
"center": "Center Align",
"center_placeholder": "centered text",
"notFound": "Code not found!",
"scriptIdNotFound": "Could not identify the script ID.",
"downloading": "Downloading...",
"downloadError": "An error occurred while downloading the script.",
"downloadTimeout": "The script download timed out.",
"info_tooltip": "Shortcuts",
"info_shortcuts_title": "Keyboard Shortcuts",
"info_header_shortcut": "Shortcut",
"info_header_action": "Action",
"info_shortcut_tab": "Inserts a tab space.",
"info_shortcut_shift_enter": "Inserts a line break <br>.",
"info_shortcut_ctrl_d": "Wraps the selection in a <div> tag.",
"info_shortcut_ctrl_p": "Wraps the selection in a <p> paragraph.",
"info_shortcut_ctrl_m": "Wraps the selection in a markdown code block.",
"info_shortcut_ctrl_space": "Inserts a non-breaking space &nbsp;.",
"prompt_link_text": "Link text:",
"prompt_abbreviation_text": "Abbreviation text:",
"border_style": "Border Style",
"prompt_border_size": "Border size (px)",
"prompt_border_color": "Border color",
"prompt_border_text": "Text",
"prompt_border_tag_type": "Tag Type",
"border_text_placeholder": "Text"
},
"pt-BR": {
"langName": "Português (BR)",
"languageSettings": "🌐 Idioma",
"close": "Fechar",
"confirm": "Confirmar",
"cancel": "Cancelar",
"download": "Baixar",
"compatible_with": "Compatível com",
"force_update": "🔄️ Forçar Atualização",
"force_update_alert": "O cache foi limpo. A página será recarregada para buscar os dados atualizados.",
"titles": "Títulos",
"title_placeholder": "Título",
"bold": "Negrito",
"bold_placeholder": "negrito",
"italic": "Itálico",
"italic_placeholder": "itálico",
"underline": "Sublinhado",
"underline_placeholder": "sublinhado",
"strikethrough": "Riscado",
"strikethrough_placeholder": "riscado",
"unordered_list": "Lista não ordenada",
"ordered_list": "Lista ordenada",
"list_item_placeholder": "Item",
"quote": "Citação",
"inline_code": "Código Inline",
"inline_code_placeholder": "código",
"code_block": "Bloco de Código",
"code_block_placeholder": "código aqui",
"horizontal_line": "Linha Horizontal",
"horizontal_line_style": "Estilo da Linha Horizontal",
"prompt_hr_size": "Tamanho (px)",
"prompt_hr_color": "Cor",
"link": "Link",
"prompt_insert_url": "Insira a URL:",
"link_text_placeholder": "texto do link",
"image": "Imagem",
"prompt_insert_image_url": "Insira a URL da imagem (https):",
"prompt_image_title": "Título da imagem (opcional):",
"image_title_placeholder": "ex: Minha bela imagem",
"prompt_image_width": "Largura (opcional):",
"prompt_image_height": "Altura (opcional):",
"video": "Vídeo",
"prompt_video_type": "Tipo de Vídeo",
"video_type_embed": "Incorporado (YouTube, Bilibili)",
"video_type_html5": "Vídeo HTML5 (URL direta)",
"prompt_video_poster_url": "URL da Imagem de Capa (poster)",
"prompt_insert_video_url": "Insira a URL do vídeo:",
"prompt_video_width": "Largura (opcional):",
"prompt_video_height": "Altura (opcional):",
"alert_invalid_video_url": "URL de vídeo inválida ou não suportada.",
"table": "Tabela",
"prompt_columns": "Número de colunas:",
"prompt_rows": "Número de linhas:",
"table_header_placeholder": "Cabeçalho",
"table_cell_placeholder": "Célula",
"subscript": "Subscrito",
"subscript_placeholder": "sub",
"superscript": "Sobrescrito",
"superscript_placeholder": "sup",
"highlight": "Marcação",
"highlight_placeholder": "marcado",
"keyboard": "Teclado",
"keyboard_placeholder": "Ctrl+C",
"abbreviation": "Abreviação",
"prompt_abbreviation_meaning": "Qual o significado da abreviação?",
"abbreviation_placeholder": "HTML",
"text_color": "Cor do Texto",
"colored_text_placeholder": "texto colorido",
"background_color": "Cor de Fundo",
"colored_background_placeholder": "fundo colorido",
"details": "Seção Recolhível",
"details_summary_placeholder": "Resumo ou Título",
"details_content_placeholder": "Conteúdo a ser ocultado...",
"center": "Centralizar",
"center_placeholder": "texto centralizado",
"notFound": "Código não encontrado!",
"scriptIdNotFound": "Não foi possível identificar o ID do script.",
"downloading": "Baixando...",
"downloadError": "Ocorreu um erro ao baixar o script.",
"downloadTimeout": "O tempo para baixar o script esgotou.",
"info_tooltip": "Atalhos",
"info_shortcuts_title": "Atalhos do Teclado",
"info_header_shortcut": "Atalho",
"info_header_action": "Ação",
"info_shortcut_tab": "Insere um espaço de tabulação.",
"info_shortcut_shift_enter": "Insere uma quebra de linha <br>.",
"info_shortcut_ctrl_d": "Envolve a seleção em uma tag <div>.",
"info_shortcut_ctrl_p": "Envolve a seleção em um parágrafo <p>.",
"info_shortcut_ctrl_m": "Envolve a seleção em um bloco de código markdown.",
"info_shortcut_ctrl_space": "Insere um espaço não separável &nbsp;.",
"prompt_link_text": "Texto do link:",
"prompt_abbreviation_text": "Texto da abreviação:",
"border_style": "Estilo da Borda",
"prompt_border_size": "Tamanho da borda (px)",
"prompt_border_color": "Cor da borda",
"prompt_border_text": "Texto",
"prompt_border_tag_type": "Tipo de Tag",
"border_text_placeholder": "Texto"
},
"es": {
"langName": "Español",
"languageSettings": "🌐 Idioma",
"close": "Cerrar",
"confirm": "Confirmar",
"cancel": "Cancelar",
"download": "Descargar",
"compatible_with": "Compatible con",
"force_update": "🔄️ Forzar actualización",
"force_update_alert": "La caché se limpió. La página se recargará para obtener los datos actualizados.",
"titles": "Títulos",
"title_placeholder": "Título",
"bold": "Negrita",
"bold_placeholder": "texto en negrita",
"italic": "Cursiva",
"italic_placeholder": "texto en cursiva",
"underline": "Subrayado",
"underline_placeholder": "texto subrayado",
"strikethrough": "Tachado",
"strikethrough_placeholder": "texto tachado",
"unordered_list": "Lista sin ordenar",
"ordered_list": "Lista ordenada",
"list_item_placeholder": "Elemento",
"quote": "Cita",
"inline_code": "Código en línea",
"inline_code_placeholder": "código",
"code_block": "Bloque de código",
"code_block_placeholder": "código aquí",
"horizontal_line": "Línea horizontal",
"horizontal_line_style": "Estilo de Línea Horizontal",
"prompt_hr_size": "Tamaño (px)",
"prompt_hr_color": "Color",
"link": "Enlace",
"prompt_insert_url": "Introduzca la URL:",
"link_text_placeholder": "texto del enlace",
"image": "Imagen",
"prompt_insert_image_url": "Introduzca la URL de la imagen (https):",
"prompt_image_title": "Título de la imagen (opcional):",
"image_title_placeholder": "ej: Mi hermosa imagen",
"prompt_image_width": "Ancho (opcional):",
"prompt_image_height": "Alto (opcional):",
"video": "Video",
"prompt_video_type": "Tipo de Video",
"video_type_embed": "Incrustado (YouTube, Bilibili)",
"video_type_html5": "Video HTML5 (URL directa)",
"prompt_video_poster_url": "URL de la imagen de portada (póster)",
"prompt_insert_video_url": "Introduzca la URL del video:",
"prompt_video_width": "Ancho (opcional):",
"prompt_video_height": "Alto (opcional):",
"alert_invalid_video_url": "URL de video no válida o no compatible.",
"table": "Tabla",
"prompt_columns": "Número de columnas:",
"prompt_rows": "Número de filas:",
"table_header_placeholder": "Encabezado",
"table_cell_placeholder": "Celda",
"subscript": "Subíndice",
"subscript_placeholder": "sub",
"superscript": "Superíndice",
"superscript_placeholder": "sup",
"highlight": "Resaltado",
"highlight_placeholder": "resaltado",
"keyboard": "Teclado",
"keyboard_placeholder": "Ctrl+C",
"abbreviation": "Abreviatura",
"prompt_abbreviation_meaning": "¿Qué significa la abreviação?",
"abbreviation_placeholder": "HTML",
"text_color": "Color del texto",
"colored_text_placeholder": "texto coloreado",
"background_color": "Color de fondo",
"colored_background_placeholder": "fondo coloreado",
"details": "Sección Plegable",
"details_summary_placeholder": "Resumen o Título",
"details_content_placeholder": "Contenido a ocultar...",
"center": "Centrar",
"center_placeholder": "texto centrado",
"notFound": "¡Código no encontrado!",
"scriptIdNotFound": "No se pudo identificar el ID del script.",
"downloading": "Descargando...",
"downloadError": "Ocurrió un error al descargar el script.",
"downloadTimeout": "Se agotó el tiempo de espera para la descarga del script.",
"info_tooltip": "Atajos",
"info_shortcuts_title": "Atajos de Teclado",
"info_header_shortcut": "Atajo",
"info_header_action": "Acción",
"info_shortcut_tab": "Inserta un espacio de tabulación.",
"info_shortcut_shift_enter": "Inserta un salto de línea <br>.",
"info_shortcut_ctrl_d": "Envuelve la selección en una etiqueta <div>.",
"info_shortcut_ctrl_p": "Envuelve la selección en un párrafo <p>.",
"info_shortcut_ctrl_m": "Envuelve la selección en un bloque de código markdown.",
"info_shortcut_ctrl_space": "Inserta un espacio no separable &nbsp;.",
"prompt_link_text": "Texto del enlace:",
"prompt_abbreviation_text": "Texto de la abreviatura:",
"border_style": "Estilo de Borde",
"prompt_border_size": "Tamaño del borde (px)",
"prompt_border_color": "Color del borde",
"prompt_border_text": "Texto",
"prompt_border_tag_type": "Tipo de Etiqueta",
"border_text_placeholder": "Texto"
},
"fr": {
"langName": "Français",
"languageSettings": "🌐 Langue",
"close": "Fermer",
"confirm": "Confirmer",
"cancel": "Annuler",
"download": "Télécharger",
"compatible_with": "Compatible avec",
"force_update": "🔄️ Forcer la mise à jour",
"force_update_alert": "Cache vidé. La page va se recharger pour récupérer les données mises à jour.",
"titles": "Titres",
"title_placeholder": "Titre",
"bold": "Gras",
"bold_placeholder": "texte en gras",
"italic": "Italique",
"italic_placeholder": "texte en italique",
"underline": "Souligné",
"underline_placeholder": "texte souligné",
"strikethrough": "Barré",
"strikethrough_placeholder": "texte barré",
"unordered_list": "Liste non ordonnée",
"ordered_list": "Liste ordonnée",
"list_item_placeholder": "Élément",
"quote": "Citation",
"inline_code": "Code en ligne",
"inline_code_placeholder": "code",
"code_block": "Bloc de code",
"code_block_placeholder": "code ici",
"horizontal_line": "Ligne horizontale",
"horizontal_line_style": "Style de ligne horizontale",
"prompt_hr_size": "Taille (px)",
"prompt_hr_color": "Couleur",
"link": "Lien",
"prompt_insert_url": "Entrez l'URL :",
"link_text_placeholder": "texte du lien",
"image": "Image",
"prompt_insert_image_url": "Entrez l'URL de l'image (https) :",
"prompt_image_title": "Titre de l'image (facultatif) :",
"image_title_placeholder": "ex. Ma belle image",
"prompt_image_width": "Largeur (facultatif) :",
"prompt_image_height": "Hauteur (facultatif) :",
"video": "Vidéo",
"prompt_video_type": "Type de vidéo",
"video_type_embed": "Intégrée (YouTube, Bilibili)",
"video_type_html5": "Vidéo HTML5 (URL directe)",
"prompt_video_poster_url": "URL de l'image d'affiche",
"prompt_insert_video_url": "Entrez l'URL de la vidéo :",
"prompt_video_width": "Largeur (facultatif) :",
"prompt_video_height": "Hauteur (facultatif) :",
"alert_invalid_video_url": "URL de vidéo invalide ou non prise en charge.",
"table": "Tableau",
"prompt_columns": "Nombre de colonnes :",
"prompt_rows": "Nombre de lignes :",
"table_header_placeholder": "En-tête",
"table_cell_placeholder": "Cellule",
"subscript": "Indice",
"subscript_placeholder": "ind",
"superscript": "Exposant",
"superscript_placeholder": "exp",
"highlight": "Surligner",
"highlight_placeholder": "surligné",
"keyboard": "Clavier",
"keyboard_placeholder": "Ctrl+C",
"abbreviation": "Abréviation",
"prompt_abbreviation_meaning": "Que signifie l'abréviation ?",
"abbreviation_placeholder": "HTML",
"text_color": "Couleur du texte",
"colored_text_placeholder": "texte coloré",
"background_color": "Couleur de fond",
"colored_background_placeholder": "fond coloré",
"details": "Section réductible",
"details_summary_placeholder": "Résumé ou Titre",
"details_content_placeholder": "Contenu à masquer...",
"center": "Aligner au centre",
"center_placeholder": "texte centré",
"notFound": "Code non trouvé !",
"scriptIdNotFound": "Impossible d'identifier l'ID du script.",
"downloading": "Téléchargement en cours...",
"downloadError": "Une erreur s'est produite lors du téléchargement du script.",
"downloadTimeout": "Le téléchargement du script a expiré.",
"info_tooltip": "Raccourcis",
"info_shortcuts_title": "Raccourcis clavier",
"info_header_shortcut": "Raccourci",
"info_header_action": "Action",
"info_shortcut_tab": "Insère une tabulation.",
"info_shortcut_shift_enter": "Insère un saut de ligne <br>.",
"info_shortcut_ctrl_d": "Enveloppe la sélection dans une balise <div>.",
"info_shortcut_ctrl_p": "Enveloppe la sélection dans un paragraphe <p>.",
"info_shortcut_ctrl_m": "Enveloppe la sélection dans un bloc de code markdown.",
"info_shortcut_ctrl_space": "Insère un espace insécable &nbsp;.",
"prompt_link_text": "Texte du lien :",
"prompt_abbreviation_text": "Texte de l'abréviation :",
"border_style": "Style de bordure",
"prompt_border_size": "Taille de la bordure (px)",
"prompt_border_color": "Couleur de la bordure",
"prompt_border_text": "Texte",
"prompt_border_tag_type": "Type de balise",
"border_text_placeholder": "Texte"
},
"zh-CN": {
"langName": "简体中文",
"languageSettings": "🌐 语言",
"close": "关闭",
"confirm": "确认",
"cancel": "取消",
"download": "下载",
"compatible_with": "兼容",
"force_update": "🔄️ 强制更新",
"force_update_alert": "缓存已清除。页面将重新加载以获取最新数据。",
"titles": "标题",
"title_placeholder": "标题",
"bold": "粗体",
"bold_placeholder": "粗体文本",
"italic": "斜体",
"italic_placeholder": "斜体文本",
"underline": "下划线",
"underline_placeholder": "下划线文本",
"strikethrough": "删除线",
"strikethrough_placeholder": "删除线文本",
"unordered_list": "无序列表",
"ordered_list": "有序列表",
"list_item_placeholder": "项目",
"quote": "引用",
"inline_code": "行内代码",
"inline_code_placeholder": "代码",
"code_block": "代码块",
"code_block_placeholder": "在此处编写代码",
"horizontal_line": "水平线",
"horizontal_line_style": "水平线样式",
"prompt_hr_size": "大小 (px)",
"prompt_hr_color": "颜色",
"link": "链接",
"prompt_insert_url": "请输入网址:",
"link_text_placeholder": "链接文本",
"image": "图片",
"prompt_insert_image_url": "请输入图片网址 (https):",
"prompt_image_title": "图片标题(可选):",
"image_title_placeholder": "例如:我美丽的图片",
"prompt_image_width": "宽度(可选):",
"prompt_image_height": "高度(可选):",
"video": "视频",
"prompt_video_type": "视频类型",
"video_type_embed": "嵌入 (YouTube, 哔哩哔哩)",
"video_type_html5": "HTML5 视频 (直接链接)",
"prompt_video_poster_url": "封面图片链接 (poster)",
"prompt_insert_video_url": "请输入视频网址:",
"prompt_video_width": "宽度(可选):",
"prompt_video_height": "高度(可选):",
"alert_invalid_video_url": "无效或不支持的视频网址。",
"table": "表格",
"prompt_columns": "列数:",
"prompt_rows": "行数:",
"table_header_placeholder": "标题",
"table_cell_placeholder": "单元格",
"subscript": "下标",
"subscript_placeholder": "下标",
"superscript": "上标",
"superscript_placeholder": "上标",
"highlight": "标记",
"highlight_placeholder": "标记",
"keyboard": "键盘",
"keyboard_placeholder": "Ctrl+C",
"abbreviation": "缩写",
"prompt_abbreviation_meaning": "缩写的含义是什么?",
"abbreviation_placeholder": "HTML",
"text_color": "文字颜色",
"colored_text_placeholder": "彩色文本",
"background_color": "背景颜色",
"colored_background_placeholder": "彩色背景",
"details": "可折叠部分",
"details_summary_placeholder": "摘要或标题",
"details_content_placeholder": "要隐藏的内容...",
"center": "居中",
"center_placeholder": "居中文字",
"notFound": "未找到代码!",
"scriptIdNotFound": "无法识别脚本 ID。",
"downloading": "下载中...",
"downloadError": "下载脚本时发生错误。",
"downloadTimeout": "脚本下载超时。",
"info_tooltip": "快捷方式",
"info_shortcuts_title": "键盘快捷键",
"info_header_shortcut": "快捷键",
"info_header_action": "功能",
"info_shortcut_tab": "插入一个制表符。",
"info_shortcut_shift_enter": "插入一个换行符 <br>。",
"info_shortcut_ctrl_d": "将所选内容包裹在 <div> 标签中。",
"info_shortcut_ctrl_p": "将所选内容包裹在 <p> 段落中。",
"info_shortcut_ctrl_m": "将所选内容包裹在 markdown 代码块中。",
"info_shortcut_ctrl_space": "插入不间断空格 &nbsp;。",
"prompt_link_text": "链接文本:",
"prompt_abbreviation_text": "缩写文本:",
"border_style": "边框样式",
"prompt_border_size": "边框大小 (px)",
"prompt_border_color": "边框颜色",
"prompt_border_text": "文本",
"prompt_border_tag_type": "标签类型",
"border_text_placeholder": "文本"
},
"ja": {
"langName": "日本語",
"languageSettings": "🌐 言語",
"close": "閉じる",
"confirm": "確認",
"cancel": "キャンセル",
"download": "ダウンロード",
"compatible_with": "互換性:",
"force_update": "🔄️ 強制アップデート",
"force_update_alert": "キャッシュがクリアされました。ページがリロードされ、更新されたデータが取得されます。",
"titles": "見出し",
"title_placeholder": "見出し",
"bold": "太字",
"bold_placeholder": "太字のテキスト",
"italic": "斜体",
"italic_placeholder": "斜体のテキスト",
"underline": "下線",
"underline_placeholder": "下線付きテキスト",
"strikethrough": "取り消し線",
"strikethrough_placeholder": "取り消し線付きテキスト",
"unordered_list": "順序なしリスト",
"ordered_list": "順序付きリスト",
"list_item_placeholder": "アイテム",
"quote": "引用",
"inline_code": "インラインコード",
"inline_code_placeholder": "コード",
"code_block": "コードブロック",
"code_block_placeholder": "ここにコード",
"horizontal_line": "水平線",
"horizontal_line_style": "水平線のスタイル",
"prompt_hr_size": "サイズ (px)",
"prompt_hr_color": "色",
"link": "リンク",
"prompt_insert_url": "URLを入力してください:",
"link_text_placeholder": "リンクテキスト",
"image": "画像",
"prompt_insert_image_url": "画像URLを入力してください (https):",
"prompt_image_title": "画像のタイトル (任意):",
"image_title_placeholder": "例: 私の美しい画像",
"prompt_image_width": "幅 (任意):",
"prompt_image_height": "高さ (任意):",
"video": "動画",
"prompt_video_type": "動画タイプ",
"video_type_embed": "埋め込み (YouTube, Bilibili)",
"video_type_html5": "HTML5 ビデオ (直接URL)",
"prompt_video_poster_url": "ポスター画像のURL",
"prompt_insert_video_url": "動画URLを入力してください:",
"prompt_video_width": "幅 (任意):",
"prompt_video_height": "高さ (任意):",
"alert_invalid_video_url": "無効またはサポートされていない動画URLです。",
"table": "表",
"prompt_columns": "列数:",
"prompt_rows": "行数:",
"table_header_placeholder": "ヘッダー",
"table_cell_placeholder": "セル",
"subscript": "下付き文字",
"subscript_placeholder": "下付き",
"superscript": "上付き文字",
"superscript_placeholder": "上付き",
"highlight": "ハイライト",
"highlight_placeholder": "ハイライトされたテキスト",
"keyboard": "キーボード",
"keyboard_placeholder": "Ctrl+C",
"abbreviation": "略語",
"prompt_abbreviation_meaning": "この略語は何の略ですか?",
"abbreviation_placeholder": "HTML",
"text_color": "文字色",
"colored_text_placeholder": "色付きテキスト",
"background_color": "背景色",
"colored_background_placeholder": "色付き背景",
"details": "折りたたみ可能なセクション",
"details_summary_placeholder": "概要またはタイトル",
"details_content_placeholder": "非表示にするコンテンツ...",
"center": "中央揃え",
"center_placeholder": "中央揃えのテキスト",
"notFound": "コードが見つかりません!",
"scriptIdNotFound": "スクリプトIDを特定できませんでした。",
"downloading": "ダウンロード中...",
"downloadError": "スクリプトのダウンロード中にエラーが発生しました。",
"downloadTimeout": "スクリプトのダウンロードがタイムアウトしました。",
"info_tooltip": "ショートカット",
"info_shortcuts_title": "キーボードショートカット",
"info_header_shortcut": "ショートカット",
"info_header_action": "アクション",
"info_shortcut_tab": "タブスペースを挿入します。",
"info_shortcut_shift_enter": "改行 <br> を挿入します。",
"info_shortcut_ctrl_d": "選択範囲を <div> タグで囲みます。",
"info_shortcut_ctrl_p": "選択範囲を <p> 段落で囲みます。",
"info_shortcut_ctrl_m": "選択範囲を Markdown コードブロックで囲みます。",
"info_shortcut_ctrl_space": "改行しないスペース &nbsp; を挿入します。",
"prompt_link_text": "リンクテキスト:",
"prompt_abbreviation_text": "略語テキスト:",
"border_style": "枠線のスタイル",
"prompt_border_size": "枠線のサイズ (px)",
"prompt_border_color": "枠線の色",
"prompt_border_text": "テキスト",
"prompt_border_tag_type": "タグタイプ",
"border_text_placeholder": "テキスト"
},
"ko": {
"langName": "한국어",
"languageSettings": "🌐 언어",
"close": "닫기",
"confirm": "확인",
"cancel": "취소",
"download": "다운로드",
"compatible_with": "호환 가능:",
"force_update": "🔄️ 강제 업데이트",
"force_update_alert": "캐시가 지워졌습니다. 페이지를 새로고침하여 업데이트된 데이터를 가져옵니다.",
"titles": "제목",
"title_placeholder": "제목",
"bold": "굵게",
"bold_placeholder": "굵은 텍스트",
"italic": "기울임꼴",
"italic_placeholder": "기울임꼴 텍스트",
"underline": "밑줄",
"underline_placeholder": "밑줄 친 텍스트",
"strikethrough": "취소선",
"strikethrough_placeholder": "취소선 텍스트",
"unordered_list": "순서 없는 목록",
"ordered_list": "순서 있는 목록",
"list_item_placeholder": "항목",
"quote": "인용",
"inline_code": "인라인 코드",
"inline_code_placeholder": "코드",
"code_block": "코드 블록",
"code_block_placeholder": "여기에 코드",
"horizontal_line": "가로줄",
"horizontal_line_style": "가로줄 스타일",
"prompt_hr_size": "크기 (px)",
"prompt_hr_color": "색상",
"link": "링크",
"prompt_insert_url": "URL을 입력하세요:",
"link_text_placeholder": "링크 텍스트",
"image": "이미지",
"prompt_insert_image_url": "이미지 URL을 입력하세요 (https):",
"prompt_image_title": "이미지 제목 (선택 사항):",
"image_title_placeholder": "예: 내 아름다운 이미지",
"prompt_image_width": "너비 (선택 사항):",
"prompt_image_height": "높이 (선택 사항):",
"video": "동영상",
"prompt_video_type": "비디오 유형",
"video_type_embed": "임베드 (YouTube, Bilibili)",
"video_type_html5": "HTML5 비디오 (직접 URL)",
"prompt_video_poster_url": "포스터 이미지 URL",
"prompt_insert_video_url": "동영상 URL을 입력하세요:",
"prompt_video_width": "너비 (선택 사항):",
"prompt_video_height": "높이 (선택 사항):",
"alert_invalid_video_url": "잘못되었거나 지원되지 않는 동영상 URL입니다.",
"table": "표",
"prompt_columns": "열 수:",
"prompt_rows": "행 수:",
"table_header_placeholder": "헤더",
"table_cell_placeholder": "셀",
"subscript": "아래 첨자",
"subscript_placeholder": "아래 첨자",
"superscript": "위 첨자",
"superscript_placeholder": "위 첨자",
"highlight": "강조",
"highlight_placeholder": "강조된 텍스트",
"keyboard": "키보드",
"keyboard_placeholder": "Ctrl+C",
"abbreviation": "약어",
"prompt_abbreviation_meaning": "이 약어는 무엇을 의미합니까?",
"abbreviation_placeholder": "HTML",
"text_color": "텍스트 색상",
"colored_text_placeholder": "색깔 있는 텍스트",
"background_color": "배경색",
"colored_background_placeholder": "색깔 있는 배경",
"details": "접을 수 있는 섹션",
"details_summary_placeholder": "요약 또는 제목",
"details_content_placeholder": "숨길 내용...",
"center": "가운데 정렬",
"center_placeholder": "가운데 정렬된 텍스트",
"notFound": "코드를 찾을 수 없습니다!",
"scriptIdNotFound": "스크립트 ID를 식별할 수 없습니다.",
"downloading": "다운로드 중...",
"downloadError": "스크립트를 다운로드하는 동안 오류가 발생했습니다.",
"downloadTimeout": "스크립트 다운로드 시간이 초과되었습니다.",
"info_tooltip": "단축키",
"info_shortcuts_title": "키보드 단축키",
"info_header_shortcut": "단축키",
"info_header_action": "동작",
"info_shortcut_tab": "탭 공백을 삽입합니다.",
"info_shortcut_shift_enter": "줄 바꿈 <br>을 삽입합니다.",
"info_shortcut_ctrl_d": "선택 항목을 <div> 태그로 래핑합니다.",
"info_shortcut_ctrl_p": "선택한 내용을 <p> 문단으로 감쌉니다.",
"info_shortcut_ctrl_m": "선택한 내용을 마크다운 코드 블록으로 감쌉니다.",
"info_shortcut_ctrl_space": "줄 바꿈되지 않는 공백 &nbsp; 을 삽입합니다.",
"prompt_link_text": "링크 텍스트:",
"prompt_abbreviation_text": "약어 텍스트:",
"border_style": "테두리 스타일",
"prompt_border_size": "테두리 크기 (px)",
"prompt_border_color": "테두리 색상",
"prompt_border_text": "텍스트",
"prompt_border_tag_type": "태그 유형",
"border_text_placeholder": "텍스트"
}
};
const translations = allTranslations;
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 CACHE_KEY = 'Values';
const LANG_STORAGE_KEY = 'UserScriptLang';
const LAST_TAG_TYPE_KEY = 'Tag';
const LAST_COLOR_KEY = 'Color';
const SCRIPT_CONFIG = {
notificationsUrl: 'https://gist.github.com/0H4S/d55d216b4487d64c606abb5d4f097fe0',
scriptVersion: '1.7',
};
const notifier = new ScriptNotifier(SCRIPT_CONFIG);
notifier.run();
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('zh')) currentLang = 'zh-CN';
else if (browserLang.startsWith('en')) currentLang = 'en';
else if (browserLang.startsWith('es')) currentLang = 'es';
else if (browserLang.startsWith('fr')) currentLang = 'fr';
else if (browserLang.startsWith('ja')) currentLang = 'ja';
else if (browserLang.startsWith('ko')) currentLang = 'ko';
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();
}
// #endregion
// ================
// #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('');
}
// #endregion
// ================
// #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 < 7 * 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();
}
// #endregion
// ================
// #region METADADOS
// ================
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; z-index: 10;';
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);
}
// #endregion
// ================
// #region EDITOR HTML
// ================
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;
if (!selected && !placeholder) {
textarea.setRangeText(prefix + suffix, start, end);
const cursorPosition = start + prefix.length;
textarea.setSelectionRange(cursorPosition, cursorPosition);
} else {
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 showCustomAlert(message) {
const overlay = document.createElement('div');
overlay.className = 'custom-prompt-overlay';
const modal = document.createElement('div');
modal.className = 'custom-prompt-box custom-alert-box';
const editorContainer = document.querySelector('.txt-editor-container');
modal.classList.add(editorContainer && editorContainer.classList.contains('dark-theme') ? 'dark-theme' : 'light-theme');
const messageP = document.createElement('p');
messageP.textContent = message;
const closeBtn = document.createElement('button');
closeBtn.textContent = getTranslation('close');
closeBtn.className = 'custom-prompt-confirm';
closeBtn.onclick = () => document.body.removeChild(overlay);
modal.append(messageP, closeBtn);
overlay.appendChild(modal);
document.body.appendChild(overlay);
closeBtn.focus();
}
function showCustomPrompt({ inputs, onConfirm }) {
const overlay = document.createElement('div');
overlay.className = 'custom-prompt-overlay';
const modal = document.createElement('div');
modal.className = 'custom-prompt-box';
const editorContainer = document.querySelector('.txt-editor-container');
modal.classList.add(editorContainer && editorContainer.classList.contains('dark-theme') ? 'dark-theme' : 'light-theme');
const form = document.createElement('form');
const inputsMap = new Map();
inputs.forEach(config => {
const label = document.createElement('label');
label.textContent = config.label;
let field;
if (config.type === 'select') {
field = document.createElement('select');
(config.options || []).forEach(opt => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.text;
if (config.value && opt.value === config.value) {
option.selected = true;
}
field.appendChild(option);
});
} else {
field = document.createElement('input');
field.type = config.type || 'text';
field.placeholder = config.placeholder || '';
field.value = config.value || '';
field.required = config.required !== false;
if (config.type === 'number') field.min = '1';
}
label.appendChild(field);
form.appendChild(label);
inputsMap.set(config.id, field);
});
const buttons = document.createElement('div');
buttons.className = 'custom-prompt-buttons';
const confirmBtn = document.createElement('button');
confirmBtn.type = 'submit';
confirmBtn.textContent = getTranslation('confirm');
confirmBtn.className = 'custom-prompt-confirm';
const cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.textContent = getTranslation('cancel');
cancelBtn.className = 'custom-prompt-cancel';
cancelBtn.onclick = () => document.body.removeChild(overlay);
form.onsubmit = (e) => {
e.preventDefault();
const results = {};
for (const [id, inputElement] of inputsMap.entries()) {
results[id] = inputElement.value;
}
onConfirm(results);
document.body.removeChild(overlay);
};
buttons.append(confirmBtn, cancelBtn);
form.appendChild(buttons);
modal.appendChild(form);
overlay.appendChild(modal);
document.body.appendChild(overlay);
inputsMap.values().next().value.focus();
}
function showInfoModal() {
const overlay = document.createElement('div');
overlay.className = 'custom-prompt-overlay info-modal-overlay';
overlay.style.display = 'flex';
const modal = document.createElement('div');
modal.className = 'custom-prompt-box info-modal-box';
const editorContainer = document.querySelector('.txt-editor-container');
modal.classList.add(editorContainer && editorContainer.classList.contains('dark-theme') ? 'dark-theme' : 'light-theme');
modal.innerHTML = `
<h2>${getTranslation('info_shortcuts_title')}</h2>
<div class="info-shortcuts">
<table>
<thead>
<tr>
<th>${getTranslation('info_header_shortcut')}</th>
<th>${getTranslation('info_header_action')}</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Tab</code></td>
<td>${getTranslation('info_shortcut_tab')}</td>
</tr>
<tr>
<td><code>Shift + Enter</code></td>
<td>${getTranslation('info_shortcut_shift_enter')}</td>
</tr>
<tr>
<td><code>Ctrl + D</code></td>
<td>${getTranslation('info_shortcut_ctrl_d')}</td>
</tr>
<tr>
<td><code>Ctrl + P</code></td>
<td>${getTranslation('info_shortcut_ctrl_p')}</td>
</tr>
<tr>
<td><code>Ctrl + M</code></td>
<td>${getTranslation('info_shortcut_ctrl_m')}</td>
</tr>
<tr>
<td><code>Ctrl + Space</code></td>
<td>${getTranslation('info_shortcut_ctrl_space')}</td>
</tr>
</tbody>
</table>
</div>
<div class="custom-prompt-buttons">
<button class="custom-prompt-cancel">${getTranslation('close')}</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
overlay.querySelector('.custom-prompt-cancel').onclick = () => document.body.removeChild(overlay);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
}
});
}
async function createTextStyleEditor(textarea) {
if (textarea.dataset.editorApplied) return;
textarea.dataset.editorApplied = 'true';
textarea.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
this.setRangeText(' ', start, end, 'end');
}
if (e.shiftKey && e.key === 'Enter') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
this.setRangeText('\n<br>\n', start, end, 'end');
}
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === 'd') {
e.preventDefault();
insertText(this, '<div>\n', '\n</div>', '');
}
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === 'p') {
e.preventDefault();
insertText(this, '<p>', '</p>', '');
}
if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === 'm') {
e.preventDefault();
insertText(this, '```\n', '\n```', '');
}
if (e.ctrlKey && !e.shiftKey && e.key === ' ') {
e.preventDefault();
insertText(this, ' ', '', '');
}
});
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': '1', 'H2': '2', 'H3': '3', 'H4': '4', 'H5': '5', 'H6': '6' }, action: (val) => insertText(textarea, `<h${val}>`, `</h${val}>`, getTranslation('title_placeholder')) },
{ type: 'divider' },
{ title: getTranslation('bold'), icon: icons.bold, action: () => insertText(textarea, '<strong>', '</strong>', getTranslation('bold_placeholder')) },
{ title: getTranslation('italic'), icon: icons.italic, action: () => insertText(textarea, '<em>', '</em>', 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, '<s>', '</s>', getTranslation('strikethrough_placeholder')) },
{ type: 'divider' },
{ title: getTranslation('unordered_list'), icon: icons.ul, action: () => { const start = textarea.selectionStart; const end = textarea.selectionEnd; const selection = textarea.value.substring(start, end); const items = selection ? selection.split('\n').map(line => ` <li>${line}</li>`).join('\n') : ` <li>${getTranslation('list_item_placeholder')}</li>`; const listHtml = `<ul>\n${items}\n</ul>`; textarea.setRangeText(listHtml, start, end, 'select'); textarea.focus(); } },
{ title: getTranslation('ordered_list'), icon: icons.ol, action: () => { const start = textarea.selectionStart; const end = textarea.selectionEnd; const selection = textarea.value.substring(start, end); const items = selection ? selection.split('\n').map(line => ` <li>${line}</li>`).join('\n') : ` <li>${getTranslation('list_item_placeholder')}</li>`; const listHtml = `<ol>\n${items}\n</ol>`; textarea.setRangeText(listHtml, start, end, 'select');textarea.focus(); } },
{ title: getTranslation('details'), label: icons.details, action: () => insertText(textarea, '\n<details>\n <summary>' + getTranslation('details_summary_placeholder') + '</summary>\n\n ' + getTranslation('details_content_placeholder') + '\n\n</details>\n') },
{ title: getTranslation('center'), label: icons.center, action: () => insertText(textarea, '\n<center>\n', '\n</center>', getTranslation('center_placeholder')) },
{ type: 'divider' },
{ title: getTranslation('quote'), icon: icons.quote, action: () => { const start = textarea.selectionStart; const end = textarea.selectionEnd; const selection = textarea.value.substring(start, end); const content = selection ? selection.replace(/\n/g, '<br>\n ') : getTranslation('quote'); const quoteHtml = `\n<blockquote>\n ${content}\n</blockquote>\n`; textarea.setRangeText(quoteHtml, start, end, 'select'); textarea.focus(); } },
{ title: getTranslation('inline_code'), icon: icons.code, action: () => insertText(textarea, '<code>', '</code>', getTranslation('inline_code_placeholder')) },
{ title: getTranslation('code_block'), label: icons.code_block, action: () => insertText(textarea, '<pre><code>', '</code></pre>', getTranslation('code_block_placeholder')) },
{ title: getTranslation('horizontal_line'), icon: icons.hr, action: () => insertText(textarea, '\n<hr>\n') },
{ type: 'divider' },
{ title: getTranslation('link'), icon: icons.link, action: () => { const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); showCustomPrompt({ inputs: [ { id: 'url', label: getTranslation('prompt_insert_url'), placeholder: 'https://', type: 'url', required: true }, { id: 'text', label: getTranslation('prompt_link_text'), placeholder: getTranslation('link_text_placeholder'), value: selectedText, required: false } ], onConfirm: ({ url, text }) => { if (url) { const linkText = text || selectedText || url; const selectionMode = selectedText ? 'end' : 'select'; textarea.setRangeText(`<a href="${url}">${linkText}</a>`, start, end, selectionMode); textarea.focus(); } } }); } },
{ title: getTranslation('image'), icon: icons.image, action: () => showCustomPrompt({ inputs: [ { id: 'src', label: getTranslation('prompt_insert_image_url'), placeholder: 'https://', type: 'url' }, { id: 'title', label: getTranslation('prompt_image_title'), placeholder: getTranslation('image_title_placeholder'), required: false }, { id: 'width', label: getTranslation('prompt_image_width'), placeholder: '500px', type: 'number', required: false }, { id: 'height', label: getTranslation('prompt_image_height'), placeholder: '500px', type: 'number', required: false } ], onConfirm: ({ src, title, width, height }) => { if (src) { const titleAttr = title ? ` title="${title}"` : ''; const widthAttr = width ? ` width="${width}"` : ''; const heightAttr = height ? ` height="${height}"` : ''; insertText(textarea, `<img src="${src}"${titleAttr}${widthAttr}${heightAttr}>`); } } }) },
{ title: getTranslation('table'), icon: icons.table, action: () => showCustomPrompt({ inputs: [ { id: 'cols', label: getTranslation('prompt_columns'), type: 'number', value: '3' }, { id: 'rows', label: getTranslation('prompt_rows'), type: 'number', value: '2' } ], onConfirm: ({ cols, rows }) => { const numCols = parseInt(cols, 10) || 3; const numRows = parseInt(rows, 10) || 2; let table = '<table>\n <thead>\n <tr>\n'; table += ' ' + Array(numCols).fill(`<th>${getTranslation('table_header_placeholder')}</th>`).join('\n ') + '\n </tr>\n </thead>\n <tbody>\n'; for (let i = 0; i < numRows; i++) { table += ' <tr>\n'; table += ' ' + Array(numCols).fill(`<td>${getTranslation('table_cell_placeholder')}</td>`).join('\n ') + '\n </tr>\n'; } table += ' </tbody>\n</table>'; insertText(textarea, `\n${table}\n`); } }) },
{ title: getTranslation('video'), icon: icons.video, action: () => showCustomPrompt({ inputs: [ { id: 'type', label: getTranslation('prompt_video_type'), type: 'select', options: [ { value: 'embed', text: getTranslation('video_type_embed') }, { value: 'html5', text: getTranslation('video_type_html5') } ]}, { id: 'url', label: getTranslation('prompt_insert_video_url'), placeholder: 'https://', type: 'url' }, { id: 'poster', label: getTranslation('prompt_video_poster_url'), placeholder: 'https://image.jpg', type: 'url', required: false }, { id: 'width', label: getTranslation('prompt_video_width'), placeholder: '560', type: 'number', required: false }, { id: 'height', label: getTranslation('prompt_video_height'), placeholder: '315', type: 'number', required: false } ], onConfirm: ({ type, url, poster, width, height }) => { if (!url) return; const widthAttr = width ? ` width="${width}"` : ''; const heightAttr = height ? ` height="${height}"` : ''; if (type === 'html5') { const posterAttr = poster ? ` poster="${poster}"` : ''; const videoTag = `\n<video src="${url}"${posterAttr}${widthAttr}${heightAttr} controls></video>\n`; insertText(textarea, videoTag); } else { let src = ''; try { 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]}`; } } catch { src = ''; } if (src) { insertText(textarea, `\n<iframe src="${src}"${widthAttr}${heightAttr} allowfullscreen></iframe>\n`); } else { showCustomAlert(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 start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); showCustomPrompt({ inputs: [ { id: 'title', label: getTranslation('prompt_abbreviation_meaning'), required: true }, { id: 'text', label: getTranslation('prompt_abbreviation_text'), placeholder: getTranslation('abbreviation_placeholder'), value: selectedText, required: true } ], onConfirm: ({ title, text }) => { if (title && text) { const selectionMode = selectedText ? 'end' : 'select'; textarea.setRangeText(`<abbr title="${title}">${text}</abbr>`, start, end, selectionMode); textarea.focus(); } } }); } },
{ type: 'color-picker' }
];
for (const tool of tools) {
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';
const lastColor = await GM_getValue(LAST_COLOR_KEY, '#58a6ff');
input.value = lastColor;
input.addEventListener('input', async (e) => {
await GM_setValue(LAST_COLOR_KEY, e.target.value);
});
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: async () => {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
const lastTag = await GM_getValue(LAST_TAG_TYPE_KEY, 'span');
showCustomPrompt({
inputs: [
{ id: 'text', label: getTranslation('prompt_border_text'), type: 'text', value: selectedText || getTranslation('colored_background_placeholder') },
{ id: 'color', label: getTranslation('background_color'), type: 'text', value: input.value },
{ id: 'tag', label: getTranslation('prompt_border_tag_type'), type: 'select', value: lastTag, options: [ { value: 'span', text: '<span>' }, { value: 'div', text: '<div>' } ] }
],
onConfirm: async ({ text, color, tag }) => {
await GM_setValue(LAST_TAG_TYPE_KEY, tag);
let newElement;
if (tag === 'div') {
newElement = `\n<div style="background-color: ${color};">\n ${text}\n</div>\n`;
} else {
newElement = `<span style="background-color: ${color};">${text}</span>`;
}
textarea.setRangeText(newElement, start, end, selectedText ? 'end' : 'select');
textarea.focus();
}
});
}
});
const hrStyleBtn = createToolbarButton({
title: getTranslation('horizontal_line_style'),
label: icons.hr_style,
action: () => {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end).trim();
let currentSize = '1';
let currentColor = input.value;
const hrRegex = /<hr\s+style="border:\s*(\d+)px\s+solid\s+([^;"]+)/i;
const match = selectedText.match(hrRegex);
if (match && selectedText.startsWith('<hr')) {
currentSize = match[1];
currentColor = match[2];
}
showCustomPrompt({
inputs: [
{ id: 'size', label: getTranslation('prompt_hr_size'), type: 'number', value: currentSize },
{ id: 'color', label: getTranslation('prompt_hr_color'), type: 'text', value: currentColor }
],
onConfirm: ({ size, color }) => {
const newHr = `<hr style="border: ${size}px solid ${color};">`;
textarea.setRangeText(newHr, start, end, 'select');
textarea.focus();
}
});
}
});
const borderStyleBtn = createToolbarButton({
title: getTranslation('border_style'),
label: icons.border,
action: async () => {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
const lastTag = await GM_getValue(LAST_TAG_TYPE_KEY, 'span');
showCustomPrompt({
inputs: [
{ id: 'text', label: getTranslation('prompt_border_text'), type: 'text', value: selectedText || getTranslation('border_text_placeholder') },
{ id: 'size', label: getTranslation('prompt_border_size'), type: 'number', value: '1' },
{ id: 'color', label: getTranslation('prompt_border_color'), type: 'text', value: input.value },
{ id: 'tag', label: getTranslation('prompt_border_tag_type'), type: 'select', value: lastTag, options: [ { value: 'span', text: '<span>' }, { value: 'div', text: '<div>' } ] }
],
onConfirm: async ({ text, size, color, tag }) => {
await GM_setValue(LAST_TAG_TYPE_KEY, tag);
let newElement;
if (tag === 'div') {
newElement = `\n<div style="border: ${size}px solid ${color};">\n ${text}\n</div>`;
} else {
newElement = `<span style="border: ${size}px solid ${color};">${text}</span>`;
}
textarea.setRangeText(newElement, start, end, selectedText ? 'end' : 'select');
textarea.focus();
}
});
}
});
colorContainer.append(input, colorBtn, bgBtn, hrStyleBtn, borderStyleBtn);
toolbar.appendChild(colorContainer);
} else {
toolbar.appendChild(createToolbarButton(tool));
}
}
const infoButton = createToolbarButton({
title: getTranslation('info_tooltip'),
icon: icons.info,
action: showInfoModal
});
infoButton.style.marginLeft = 'auto';
toolbar.appendChild(infoButton);
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));
}
// #endregion
// ================
// #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);
});
}
// #endregion
// ================
// #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();
// #endregion
})();