Greasy Fork 支持简体中文。

YouTube Customizable Subtitles / Youtube Subtitulos Personalizables

Allows you to customize YouTube subtitles / Le permite personalizar los subtítulos de YouTube.

// ==UserScript==
// @name         YouTube Customizable Subtitles / Youtube Subtitulos Personalizables
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Allows you to customize YouTube subtitles / Le permite personalizar los subtítulos de YouTube.
// @author       Eterve Nallo - Diam
// @license      MIT
// @match        *://*.youtube.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    const languages = {
        en: {
            configTitle: 'Subtitle Settings',
            outlineColor: 'Outline Color:',
            outlineWidth: 'Outline Width:',
            textColor: 'Text Color:',
            saveButton: 'Save',
            closeButton: 'X',
            languageButton: 'Language',
            languageLabel: 'Choose Language',
            menuCommand: 'Show/Hide Subtitle Settings'
        },
        es: {
            configTitle: 'Configuración de Subtítulos',
            outlineColor: 'Color del Contorno:',
            outlineWidth: 'Grosor del Contorno:',
            textColor: 'Color del Texto:',
            saveButton: 'Guardar',
            closeButton: 'X',
            languageButton: 'Idioma',
            languageLabel: 'Elegir Idioma',
            menuCommand: 'Mostrar/Ocultar Configuración de Subtítulos'
        },
        ja: {
            configTitle: '字幕設定',
            outlineColor: 'アウトラインカラー:',
            outlineWidth: 'アウトラインの幅:',
            textColor: '文字色:',
            saveButton: '保存',
            closeButton: 'X',
            languageButton: '言語',
            languageLabel: '言語を選択',
            menuCommand: '字幕設定を表示/非表示'
        },
        ru: {
            configTitle: 'Настройки субтитров',
            outlineColor: 'Цвет контура:',
            outlineWidth: 'Ширина контура:',
            textColor: 'Цвет текста:',
            saveButton: 'Сохранить',
            closeButton: 'X',
            languageButton: 'Язык',
            languageLabel: 'Выберите язык',
            menuCommand: 'Показать/Скрыть настройки субтитров'
        },
        ko: {
            configTitle: '자막 설정',
            outlineColor: '윤곽선 색상:',
            outlineWidth: '윤곽선 너비:',
            textColor: '글자 색상:',
            saveButton: '저장',
            closeButton: 'X',
            languageButton: '언어',
            languageLabel: '언어 선택',
            menuCommand: '자막 설정 표시/숨기기'
        },
        zh: {
            configTitle: '字幕设置',
            outlineColor: '轮廓颜色:',
            outlineWidth: '轮廓宽度:',
            textColor: '文字颜色:',
            saveButton: '保存',
            closeButton: 'X',
            languageButton: '语言',
            languageLabel: '选择语言',
            menuCommand: '显示/隐藏字幕设置'
        }
    };

    const defaultConfig = {
        outlineColor: 'black',
        outlineWidth: 1,
        textColor: 'white',
        showPanel: false,
        language: 'en'
    };

    function loadConfig() {
        const savedConfig = GM_getValue('ytSubtitleConfig');
        return savedConfig ? JSON.parse(savedConfig) : defaultConfig;
    }

    function saveConfig(config) {
        GM_setValue('ytSubtitleConfig', JSON.stringify(config));
    }

    const config = loadConfig();
    const lang = languages[config.language] || languages.en;

    function applySubtitleStyles() {
        const shadowOffset = `${config.outlineWidth / 2}px`;

        GM_addStyle(`
            .ytp-caption-segment {
                color: ${config.textColor} !important;
                text-shadow:
                    ${shadowOffset} ${shadowOffset} 0 ${config.outlineColor},
                    -${shadowOffset} -${shadowOffset} 0 ${config.outlineColor},
                    ${shadowOffset} -${shadowOffset} 0 ${config.outlineColor},
                    -${shadowOffset} ${shadowOffset} 0 ${config.outlineColor};
                background: transparent !important;
                font-weight: bold;
                font-size: calc(16px + 1vw);
            }
        `);
    }

function createConfigPanel() {
    const videoPlayer = document.querySelector('.html5-video-player');
    if (!videoPlayer) {
        console.error('No se encontró el reproductor de video');
        return;
    }

    const panel = document.createElement('div');
    panel.id = 'subtitleConfigPanel';
    panel.style.position = 'absolute';
    panel.style.top = '10px';
    panel.style.right = '10px';
    panel.style.padding = '10px';
    panel.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
    panel.style.color = 'white';
    panel.style.borderRadius = '5px';
    panel.style.zIndex = '9999';
    panel.style.display = 'block';

    panel.innerHTML = `
        <h4>${lang.configTitle}</h4>
        <label>${lang.outlineColor}
            <input type="color" id="outlineColor" value="${config.outlineColor}">
        </label><br>
        <label>${lang.outlineWidth}
            <div id="outlineWidthControl" style="display: flex; align-items: center;">
                <input type="range" id="outlineWidthSlider" min="0.5" max="5" step="0.5" value="${config.outlineWidth}" style="width: 150px;">
                <div id="outlineWidthDisplay" style="margin-left: 10px; font-size: 16px; width: 35px; text-align: center;">${config.outlineWidth}</div>
            </div>
        </label><br>
        <label>${lang.textColor}
            <input type="color" id="textColor" value="${config.textColor}">
        </label><br>
        <button id="saveConfig">${lang.saveButton}</button>
        <button id="closePanel" style="margin-top: 10px;">${lang.closeButton}</button>
        <br><button id="changeLanguage">${lang.languageButton}</button>
    `;

    videoPlayer.appendChild(panel);

    // Actualizar el grosor del contorno en tiempo real con el slider
    document.getElementById('outlineWidthSlider').addEventListener('input', (event) => {
        const newOutlineWidth = parseFloat(event.target.value);
        document.getElementById('outlineWidthDisplay').textContent = newOutlineWidth;
        config.outlineWidth = newOutlineWidth;
        applySubtitleStyles();
    });

    document.getElementById('saveConfig').addEventListener('click', () => {
        config.outlineColor = document.getElementById('outlineColor').value;
        config.textColor = document.getElementById('textColor').value;
        applySubtitleStyles();
        saveConfig(config);
    });

    document.getElementById('closePanel').addEventListener('click', () => {
        panel.remove();
        config.showPanel = false;
        saveConfig(config);
    });

    document.getElementById('changeLanguage').addEventListener('click', () => {
        toggleLanguageMenu(panel);
    });
}

function toggleLanguageMenu(panel) {
    const languageMenu = panel.querySelector('#languageMenu');

    if (!languageMenu) {
        // Crear nueva ventana de idiomas
        const languageMenu = document.createElement('div');
        languageMenu.id = 'languageMenu';
        languageMenu.style.position = 'absolute';
        languageMenu.style.top = '50px';
        languageMenu.style.left = '10px';
        languageMenu.style.padding = '10px';
        languageMenu.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
        languageMenu.style.color = 'white';
        languageMenu.style.borderRadius = '5px';
        languageMenu.style.zIndex = '9999';
        languageMenu.innerHTML = `
            <h4>${lang.languageLabel}</h4>
            <button id="language_en">English</button>
            <button id="language_es">Español</button>
            <button id="language_ja">日本語</button>
            <button id="language_ru">Русский</button>
            <button id="language_ko">한국어</button>
            <button id="language_zh">中文</button>
            <button id="closeLanguageMenu" style="margin-top: 10px;">${lang.closeButton}</button>
        `;

        panel.appendChild(languageMenu);

        // Añadir eventos a los botones de idioma
        document.getElementById('language_en').addEventListener('click', () => {
            changeLanguage('en');
        });
        document.getElementById('language_es').addEventListener('click', () => {
            changeLanguage('es');
        });
        document.getElementById('language_ja').addEventListener('click', () => {
            changeLanguage('ja');
        });
        document.getElementById('language_ru').addEventListener('click', () => {
            changeLanguage('ru');
        });
        document.getElementById('language_ko').addEventListener('click', () => {
            changeLanguage('ko');
        });
        document.getElementById('language_zh').addEventListener('click', () => {
            changeLanguage('zh');
        });

        // Cerrar el menú de idiomas
        document.getElementById('closeLanguageMenu').addEventListener('click', () => {
            languageMenu.remove();
        });
    }
}

function changeLanguage(newLanguage) {
    config.language = newLanguage;
    saveConfig(config);
    location.reload(); // Recarga la página para aplicar el nuevo idioma
}

function toggleConfigPanel() {
    const videoPlayer = document.querySelector('.html5-video-player');
    if (!videoPlayer) {
        console.error('No se encontró el reproductor de video');
        return;
    }

    const panel = document.getElementById('subtitleConfigPanel');
    if (panel) {
        panel.remove();
        config.showPanel = false;
    } else {
        createConfigPanel();
    }

    saveConfig(config);
}

GM_registerMenuCommand(lang.menuCommand, toggleConfigPanel);

applySubtitleStyles();
if (config.showPanel) {
    createConfigPanel();
}
})();