Netflix Subtitle Customizer

Customize Netflix subtitles appearance

目前為 2025-06-15 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Netflix Subtitle Customizer
// @namespace    https://greasyfork.org/de/users/1476487-hyperr
// @version      1.0 beta
// @description  Customize Netflix subtitles appearance
// @author       HYPERR.
// @match        https://www.netflix.com/watch/*
// @icon         https://www.netflix.com/favicon.ico
// @license      MIT
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // Default settings
    const defaults = {
        fontSize: '100%',
        fontFamily: 'Netflix Sans',
        textColor: '#FFFFFF',
        backgroundColor: '#000000',
        backgroundOpacity: '0.5',
        textShadow: '0px 0px 4px #000000',
        bold: false,
        italic: false
    };

    // Load or initialize settings
    let settings = Object.assign({}, defaults, GM_getValue('subtitleSettings', {}));

    // CSS for subtitles
    GM_addStyle(`
        .player-timedtext-text-container span {
            font-size: ${settings.fontSize} !important;
            font-family: ${settings.fontFamily} !important;
            color: ${settings.textColor} !important;
            background-color: rgba(${hexToRgb(settings.backgroundColor)}, ${settings.backgroundOpacity}) !important;
            text-shadow: ${settings.textShadow} !important;
            font-weight: ${settings.bold ? 'bold' : 'normal'} !important;
            font-style: ${settings.italic ? 'italic' : 'normal'} !important;
        }
    `);

    // Create control panel
    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'subtitle-control-panel';
        panel.style.position = 'fixed';
        panel.style.bottom = '20px';
        panel.style.right = '20px';
        panel.style.zIndex = '9999';
        panel.style.background = '#333';
        panel.style.padding = '10px';
        panel.style.borderRadius = '5px';
        panel.style.color = 'white';

        panel.innerHTML = `
            <h3 style="margin-top:0;">Subtitle Settings</h3>
            <div>
                <label>Font Size: </label>
                <input type="range" id="fontSize" min="50" max="200" value="${parseInt(settings.fontSize)}">
                <span id="fontSizeValue">${settings.fontSize}</span>%
            </div>
            <div>
                <label>Text Color: </label>
                <input type="color" id="textColor" value="${settings.textColor}">
            </div>
            <div>
                <label>Background: </label>
                <input type="color" id="backgroundColor" value="${settings.backgroundColor}">
                <input type="range" id="backgroundOpacity" min="0" max="1" step="0.1" value="${settings.backgroundOpacity}">
            </div>
            <div>
                <label><input type="checkbox" id="bold" ${settings.bold ? 'checked' : ''}> Bold</label>
                <label><input type="checkbox" id="italic" ${settings.italic ? 'checked' : ''}> Italic</label>
            </div>
            <button id="resetSettings">Reset Defaults</button>
        `;

        document.body.appendChild(panel);

        // Event listeners
        document.getElementById('fontSize').addEventListener('input', (e) => {
            const value = e.target.value + '%';
            document.getElementById('fontSizeValue').textContent = value;
            updateSetting('fontSize', value);
        });

        document.getElementById('textColor').addEventListener('input', (e) => {
            updateSetting('textColor', e.target.value);
        });

        document.getElementById('backgroundColor').addEventListener('input', (e) => {
            updateSetting('backgroundColor', e.target.value);
        });

        document.getElementById('backgroundOpacity').addEventListener('input', (e) => {
            updateSetting('backgroundOpacity', e.target.value);
        });

        document.getElementById('bold').addEventListener('change', (e) => {
            updateSetting('bold', e.target.checked);
        });

        document.getElementById('italic').addEventListener('change', (e) => {
            updateSetting('italic', e.target.checked);
        });

        document.getElementById('resetSettings').addEventListener('click', () => {
            settings = Object.assign({}, defaults);
            GM_setValue('subtitleSettings', settings);
            location.reload();
        });
    }

    // Helper function to update settings
    function updateSetting(key, value) {
        settings[key] = value;
        GM_setValue('subtitleSettings', settings);
        location.reload();
    }

    // Helper function to convert hex to rgb
    function hexToRgb(hex) {
        const r = parseInt(hex.slice(1, 3), 16);
        const g = parseInt(hex.slice(3, 5), 16);
        const b = parseInt(hex.slice(5, 7), 16);
        return `${r}, ${g}, ${b}`;
    }

    // Wait for Netflix player to load
    const observer = new MutationObserver(() => {
        if (document.querySelector('.player-timedtext-text-container')) {
            observer.disconnect();
            createControlPanel();
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // Add menu command for Tampermonkey
    GM_registerMenuCommand('Configure Subtitle Settings', () => {
        const panel = document.getElementById('subtitle-control-panel');
        if (panel) {
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        }
    });
})();