YouTube Customizable Subtitles / Youtube Subtitulos Personalizables

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

目前為 2024-08-18 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube Customizable Subtitles / Youtube Subtitulos Personalizables
// @namespace    http://tampermonkey.net/
// @version      1.3
// @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: '1px',
        textColor: 'white',
        showPanel: false,
        language: 'en'
    };

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

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

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

    function applySubtitleStyles() {
        const shadowOffset = `${parseFloat(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 panel = document.createElement('div');
        panel.id = 'subtitleConfigPanel';
        panel.style.position = 'fixed';
        panel.style.bottom = '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}
                <input type="number" id="outlineWidth" min="1" max="10" step="1" value="${parseFloat(config.outlineWidth)}">
            </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>
        `;

        document.body.appendChild(panel);

        document.getElementById('saveConfig').addEventListener('click', () => {
            config.outlineColor = document.getElementById('outlineColor').value;
            config.outlineWidth = `${Math.max(1, Math.min(parseFloat(document.getElementById('outlineWidth').value), 10))}px`;
            config.textColor = document.getElementById('textColor').value;
            applySubtitleStyles();
            saveConfig(config);
        });

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

        document.getElementById('changeLanguage').addEventListener('click', () => {
            const langPanel = document.createElement('div');
            langPanel.id = 'languageMenu';
            langPanel.style.position = 'fixed';
            langPanel.style.bottom = '10px';
            langPanel.style.right = '150px';
            langPanel.style.padding = '10px';
            langPanel.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
            langPanel.style.color = 'white';
            langPanel.style.borderRadius = '5px';
            langPanel.style.zIndex = '10000';

            langPanel.innerHTML = `
                <h4>${lang.languageLabel}</h4>
                <button id="langEn">English</button><br>
                <button id="langEs">Español</button><br>
                <button id="langJa">日本語</button><br>
                <button id="langRu">Русский</button><br>
                <button id="langKo">한국어</button><br>
                <button id="langZh">中文</button>
                <button id="closeLangMenu" style="margin-top: 10px;">${lang.closeButton}</button>
            `;

            document.body.appendChild(langPanel);

            document.getElementById('closeLangMenu').addEventListener('click', () => {
                closeLanguageMenu();
            });

            function setLanguage(language) {
                config.language = language;
                saveConfig(config);
                location.reload();
            }

            document.getElementById('langEn').addEventListener('click', () => setLanguage('en'));
            document.getElementById('langEs').addEventListener('click', () => setLanguage('es'));
            document.getElementById('langJa').addEventListener('click', () => setLanguage('ja'));
            document.getElementById('langRu').addEventListener('click', () => setLanguage('ru'));
            document.getElementById('langKo').addEventListener('click', () => setLanguage('ko'));
            document.getElementById('langZh').addEventListener('click', () => setLanguage('zh'));

            document.addEventListener('click', function(event) {
                if (!langPanel.contains(event.target) && event.target.id !== 'changeLanguage') {
                    closeLanguageMenu();
                }
            });

            function closeLanguageMenu() {
                const langMenu = document.getElementById('languageMenu');
                if (langMenu) {
                    langMenu.remove();
                }
            }
        });
    }

    function toggleConfigPanel() {
        const panel = document.getElementById('subtitleConfigPanel');
        if (panel) {
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
            config.showPanel = panel.style.display === 'block';
            saveConfig(config);
        } else {
            createConfigPanel();
        }
    }

    GM_registerMenuCommand(lang.menuCommand, toggleConfigPanel);

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