YouTube Customizable Subtitles / Youtube Subtitulos Personalizables

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

Verzia zo dňa 12.02.2025. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==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();
}
})();