Feature request: adjustable subtitle font size (with working userscript as proof-of-concept)

Hi all,

One thing I keep wanting in the web player is control over subtitle font size, independently for the learning-language line and the translation line. The current fixed sizing is too small for me on a large screen, and there is no setting to change it.

Rather than just ask, I built a working Tampermonkey userscript that does it, so you can see exactly what I mean and (hopefully) fold it in natively. What it does:

  • Adds a small button next to the Auto-pause control that opens a panel with two sliders: learning-language size and translation size.
  • Remembers both sizes across sessions.
  • Works in both the normal player and fullscreen mode.
  • Also fixes subtitle centering/alignment and removes the background box behind the text.

Full script is below. It is about 100 lines, MIT licensed, do whatever you like with it. The CSS selectors (.media-wrap .dc-orig, .dc-translit, .main-translation-wrap, etc.) are obviously tied to the current DOM, so it is meant as a reference for a native implementation, not a long-term hack.

I would have sent a pull request instead, but I could not locate the source code anywhere public. If there is a repo, point me to it and I will gladly open a PR.

Would love to see size sliders land as a real setting. Happy to help test.

Thanks for a great tool,
Alfredo

// ==UserScript==
// @name         Subtitle Size Master - v1.4
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Persistent Subtitle settings (Standard & Fullscreen support with fixed alignment)
// @author       Alfredo Canziani
// @match        *://www.languagereactor.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // 1. CSS Applicator
    window.applySubtitleFix = function(zhV, enV) {
        let styleEl = document.getElementById('tm-subtitle-fix-style');
        if (!styleEl) {
            styleEl = document.createElement('style');
            styleEl.id = 'tm-subtitle-fix-style';
            document.head.appendChild(styleEl);
        }

        styleEl.innerHTML = `
            .media-wrap .bottom-panel { display: block !important; text-align: center !important; width: 100% !important; position: absolute !important; bottom: 2% !important; left: 0 !important; height: auto !important; max-height: 40vh !important; overflow: visible !important; }
            .media-wrap .dc-orig, .media-wrap .lln-word .dc-down, .media-wrap .sentence-view .dc-down { font-size: ${zhV}px !important; line-height: 1.1 !important; display: block !important; text-align: center !important; margin: 0 auto !important; }
            .media-wrap .sentence-view .lln-not-word { font-size: ${zhV}px !important; line-height: 1.1 !important; display: inline-block !important; vertical-align: bottom !important; margin: 0 0.1em !important; }
            .media-wrap .dc-translit, .media-wrap .lln-word .dc-up, .media-wrap .sentence-view .dc-up { font-size: ${zhV * 0.4}px !important; line-height: 1.1 !important; display: block !important; text-align: center !important; margin-bottom: 2px !important; }
            .media-wrap .main-translation-wrap, .media-wrap .main-translation-wrap span, .media-wrap [lang="en"] { font-size: ${enV}px !important; line-height: 1.4 !important; display: block !important; text-align: center !important; margin: 5px auto 0 !important; }
            .media-wrap .lln-word, .media-wrap .dc-layer { display: inline-block !important; vertical-align: bottom !important; margin: 0 0.15em !important; text-align: center !important; width: auto !important; }
            .media-wrap .sentence-row, .media-wrap .sentence-view { display: inline-block !important; white-space: nowrap !important; width: auto !important; max-width: 98vw !important; margin: 0 auto !important; }
            .media-wrap .MuiPaper-root, .media-wrap .sentence-wrap-main { background-color: transparent !important; box-shadow: none !important; border: none !important; }
        `;
        localStorage.setItem('sub-zh-size', zhV);
        localStorage.setItem('sub-en-size', enV);
    };

    // 2. Multi-Target Injection Logic
    function injectSettings() {
        const standardAp = document.querySelector('.main-toolbar [aria-label="Auto-pause"]');
        const fullscreenAp = document.querySelector('.video-subs-wrap-overvideo [aria-label="Auto-pause"]');

        [standardAp, fullscreenAp].forEach((apWrapper, index) => {
            if (!apWrapper) return;

            const uniqueId = `persistent-settings-${index}`;
            if (document.getElementById(uniqueId)) return;

            const savedZh = localStorage.getItem('sub-zh-size') || 122;
            const savedEn = localStorage.getItem('sub-en-size') || 25;

            const container = document.createElement('div');
            container.id = uniqueId;

            if (index === 1) {
                // Fullscreen/Over-video: Place BELOW AP
                container.style.cssText = 'display: flex; position: relative; align-items: center; justify-content: center; margin-top: 8px; pointer-events: auto;';
            } else {
                // Standard Title Bar: Place to the RIGHT of AP
                container.style.cssText = 'display: inline-flex; position: relative; align-items: center; margin-left: 8px; pointer-events: auto;';
            }

            const btn = document.createElement('button');
            btn.className = 'MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeSmall';
            btn.style.cssText = 'color: rgba(255, 255, 255, 0.7); background: transparent; border: 1px solid rgba(255, 255, 255, 0.3); border-radius: 4px; padding: 2px 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; height: 24px; width: auto; min-width: 32px;';
            btn.innerHTML = '<span style="font-size: 10px; font-weight: bold;">ZH/EN</span>';

            const panel = document.createElement('div');
            // FIX: Align "right: 0" so it doesn't extend past the screen edge
            const panelTop = (index === 1) ? '-165px' : '30px';
            const panelRight = '0px';

            panel.style.cssText = `position: absolute; top: ${panelTop}; right: ${panelRight}; background: rgba(24, 26, 27, 0.95); border: 1px solid rgba(232, 230, 227, 0.2); border-radius: 8px; padding: 12px; width: 180px; display: none; flex-direction: column; gap: 10px; z-index: 10000; color: #fff; box-shadow: 0 4px 15px rgba(0,0,0,0.5);`;
            panel.innerHTML = `
                <div style="font-size: 11px;">ZH: <span class="zh-val-display">${savedZh}</span>px</div>
                <input type="range" class="zh-range-input" min="20" max="250" value="${savedZh}" style="width: 100%;">
                <div style="font-size: 11px;">EN: <span class="en-val-display">${savedEn}</span>px</div>
                <input type="range" class="en-range-input" min="10" max="100" value="${savedEn}" style="width: 100%;">
            `;

            const update = () => {
                const zh = container.querySelector('.zh-range-input').value;
                const en = container.querySelector('.en-range-input').value;

                document.querySelectorAll('.zh-val-display').forEach(el => el.innerText = zh);
                document.querySelectorAll('.en-val-display').forEach(el => el.innerText = en);
                document.querySelectorAll('.zh-range-input').forEach(el => el.value = zh);
                document.querySelectorAll('.en-range-input').forEach(el => el.value = en);

                window.applySubtitleFix(zh, en);
            };

            btn.onclick = (e) => {
                e.stopPropagation();
                const isHidden = panel.style.display === 'none';
                panel.style.display = isHidden ? 'flex' : 'none';
            };

            panel.querySelector('.zh-range-input').oninput = update;
            panel.querySelector('.en-range-input').oninput = update;
            document.addEventListener('click', () => panel.style.display = 'none');
            panel.onclick = (e) => e.stopPropagation();

            container.appendChild(btn);
            container.appendChild(panel);
            apWrapper.parentNode.insertBefore(container, apWrapper.nextSibling);
        });

        const currentZh = localStorage.getItem('sub-zh-size') || 122;
        const currentEn = localStorage.getItem('sub-en-size') || 25;
        window.applySubtitleFix(currentZh, currentEn);
    }

    const observer = new MutationObserver(() => injectSettings());
    observer.observe(document.body, { childList: true, subtree: true });
    injectSettings();
})();

Hi @Atcold, thank you for taking the time to do this. This is a really useful feature for MediaFile. I’ve passed this along to the team so they can discuss it!

Thanks, @Yana!

Let me know if I can contribute to your code base.
I can help with more improvements, if you like my work!

Atcold