Greasy Fork is available in English.
Improves YouTube subtitles with smarter line wrapping, readable styling, customizable settings panel, and YouTube header icon.
// ==UserScript==
// @name YouTube Subtitle Fix
// @namespace https://github.com/SDavid33
// @version 1.2.10
// @description Improves YouTube subtitles with smarter line wrapping, readable styling, customizable settings panel, and YouTube header icon.
// @author David33
// @match https://www.youtube.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_notification
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const THEME = {
accent: '#ffb330',
activeText: '#111111'
};
const DEFAULT_SETTINGS = {
enabled: true,
textColor: '#ffb330',
backgroundColor: '0, 0, 0',
backgroundOpacity: 1,
fontSizeNormal: 32,
fontSizeFullscreen: 42,
subtitleSizeMode: 'script',
preferYouTubeSizeInSmallPlayers: true,
customFontSizeNormal: 32,
customFontSizeFullscreen: 42,
lineHeight: 1.35,
maxWidthPercent: 75,
enableAutoLineBreaks: true,
preserveAutoGeneratedCaptions: true,
maxCharsPerLine: 45,
perLineBackground: true,
paddingY: 0.20,
paddingX: 0.40,
borderRadius: 4,
offsetNormal: 50,
offsetFullscreen: 80,
extraPerLine: 5,
positionNormal: 0,
positionFullscreen: 0,
textShadow: '0 0 2px rgba(0,0,0,1), 0 0 4px rgba(0,0,0,1)'
};
const SETTINGS_STORAGE_KEY = 'ytSubtitleFixSettings';
const SETTINGS = {
...DEFAULT_SETTINGS,
...loadSavedSettings()
};
const STYLE_ID = 'yt-subtitle-full-control-style';
const LINE_BACKGROUND_CLASS = 'yt-sub-fix-line-background';
const IDS = {
topButton: 'yt-sub-fix-top-button',
fallbackButton: 'yt-sub-fix-floating-button',
panel: 'yt-sub-fix-settings-panel',
uiStyle: 'yt-sub-fix-ui-style',
subtitleStyle: STYLE_ID
};
const KOFI = {
url: 'https://ko-fi.com/N4N0XO52O',
label: 'Support me on Ko-fi',
color: THEME.accent
};
let playerObserver = null;
let captionObserver = null;
let rafId = null;
let stylesInjected = false;
let observersPaused = false;
let observerResumeId = null;
let clickLock = false;
function loadSavedSettings() {
try {
if (typeof GM_getValue === 'function') {
const raw = GM_getValue(SETTINGS_STORAGE_KEY, '{}');
return JSON.parse(raw || '{}');
}
} catch (error) {
console.warn('YouTube Subtitle Fix: failed to load saved settings.', error);
}
return {};
}
function saveSettings(partialSettings) {
if (typeof GM_setValue !== 'function') return false;
Object.assign(SETTINGS, partialSettings);
GM_setValue(SETTINGS_STORAGE_KEY, JSON.stringify(SETTINGS));
return true;
}
function clampNumber(value, min, max, fallback) {
if (!Number.isFinite(value)) return fallback;
return Math.min(max, Math.max(min, value));
}
function sanitizeHexColor(value, fallback) {
const raw = String(value || '').trim();
if (/^#[0-9a-f]{6}$/i.test(raw)) return raw.toLowerCase();
if (/^[0-9a-f]{6}$/i.test(raw)) return `#${raw.toLowerCase()}`;
return fallback;
}
function rgbStringToHex(rgb) {
const parts = String(rgb || '0, 0, 0')
.split(',')
.map(v => Math.max(0, Math.min(255, Number(v.trim()) || 0)));
const [r, g, b] = parts;
return '#' + [r, g, b]
.map(v => v.toString(16).padStart(2, '0'))
.join('');
}
function hexToRgbString(hex) {
const clean = sanitizeHexColor(hex, '#000000').replace('#', '');
const r = parseInt(clean.slice(0, 2), 16);
const g = parseInt(clean.slice(2, 4), 16);
const b = parseInt(clean.slice(4, 6), 16);
return `${r}, ${g}, ${b}`;
}
function promptNumber(message, currentValue) {
const input = window.prompt(message, String(currentValue));
if (input === null) return null;
const value = Number(input.trim());
if (!Number.isFinite(value) || value <= 0) {
window.alert('Please enter a valid positive number.');
return null;
}
return value;
}
function notify(message) {
console.log('[YouTube Subtitle Fix]', message);
try {
GM_notification({
title: 'YouTube Subtitle Fix',
text: message,
timeout: 2200
});
} catch {
console.log(message);
}
}
function injectUiStyles() {
if (document.getElementById(IDS.uiStyle)) return;
const style = document.createElement('style');
style.id = IDS.uiStyle;
style.textContent = `
#${IDS.topButton} {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
height: 36px !important;
padding: 0 14px !important;
margin-left: 0 !important;
margin-right: 10px !important;
border-radius: 18px !important;
background: var(--yt-spec-badge-chip-background, rgba(255,255,255,.1)) !important;
color: var(--yt-spec-text-primary, #fff) !important;
font-family: Roboto, Arial, sans-serif !important;
font-size: 14px !important;
font-weight: 600 !important;
cursor: pointer !important;
user-select: none !important;
white-space: nowrap !important;
}
#${IDS.topButton}:hover {
background: var(--yt-spec-button-chip-background-hover, rgba(255,255,255,.18)) !important;
}
#${IDS.fallbackButton} {
position: fixed !important;
right: 18px !important;
bottom: 18px !important;
z-index: 2147483647 !important;
padding: 10px 13px !important;
border-radius: 999px !important;
background: ${THEME.accent} !important;
color: ${THEME.activeText} !important;
font-family: Roboto, Arial, sans-serif !important;
font-size: 13px !important;
font-weight: 800 !important;
cursor: pointer !important;
box-shadow: 0 8px 24px rgba(0,0,0,.45) !important;
user-select: none !important;
pointer-events: auto !important;
}
#${IDS.panel} {
position: fixed !important;
right: 18px !important;
top: 62px !important;
bottom: auto !important;
z-index: 2147483647 !important;
width: 455px !important;
max-height: calc(100vh - 82px) !important;
overflow-y: auto !important;
padding: 14px !important;
border-radius: 16px !important;
background: rgba(22, 22, 22, .98) !important;
border: 1px solid rgba(255,255,255,.18) !important;
color: #fff !important;
font-family: Roboto, Arial, sans-serif !important;
box-shadow: 0 10px 34px rgba(0,0,0,.55) !important;
box-sizing: border-box !important;
pointer-events: auto !important;
}
#${IDS.panel} .yt-sf-header {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
margin-bottom: 12px !important;
}
#${IDS.panel} .yt-sf-title {
font-size: 15px !important;
font-weight: 800 !important;
}
#${IDS.panel} .yt-sf-close {
width: 28px !important;
height: 28px !important;
border-radius: 999px !important;
border: none !important;
background: rgba(255,255,255,.12) !important;
color: #fff !important;
cursor: pointer !important;
font-size: 16px !important;
}
#${IDS.panel} .yt-sf-section {
margin-top: 12px !important;
padding-top: 12px !important;
border-top: 1px solid rgba(255,255,255,.1) !important;
}
#${IDS.panel} .yt-sf-section.no-border {
border-top: none !important;
padding-top: 0 !important;
margin-top: 0 !important;
}
#${IDS.panel} .yt-sf-label {
font-size: 12px !important;
color: #aaa !important;
margin-bottom: 7px !important;
font-weight: 700 !important;
}
#${IDS.panel} .yt-sf-row {
display: flex !important;
flex-wrap: wrap !important;
gap: 7px !important;
align-items: center !important;
}
#${IDS.panel} button {
border: none !important;
border-radius: 999px !important;
padding: 8px 11px !important;
background: rgba(255,255,255,.13) !important;
color: #fff !important;
cursor: pointer !important;
font-size: 12px !important;
font-weight: 650 !important;
}
#${IDS.panel} button.active {
background: ${THEME.accent} !important;
color: ${THEME.activeText} !important;
}
#${IDS.panel} button.danger {
background: rgba(255, 70, 70, .18) !important;
}
#${IDS.panel} .yt-sf-color-wrap {
display: inline-flex !important;
align-items: center !important;
gap: 7px !important;
flex: 0 0 auto !important;
padding: 6px 8px !important;
border-radius: 999px !important;
background: rgba(255,255,255,.13) !important;
color: #fff !important;
font-size: 12px !important;
font-weight: 650 !important;
}
#${IDS.panel} input[type="color"] {
width: 28px !important;
height: 24px !important;
padding: 0 !important;
border: none !important;
background: transparent !important;
cursor: pointer !important;
}
#${IDS.panel} .yt-sf-range-wrap {
display: flex !important;
align-items: center !important;
gap: 8px !important;
flex: 0 1 190px !important;
min-width: 190px !important;
padding: 8px 10px !important;
border-radius: 12px !important;
background: rgba(255,255,255,.08) !important;
box-sizing: border-box !important;
}
#${IDS.panel} .yt-sf-range-wrap span:first-child {
white-space: nowrap !important;
}
#${IDS.panel} input[type="range"] {
flex: 1 !important;
min-width: 0 !important;
width: 80px !important;
accent-color: ${THEME.accent} !important;
}
#${IDS.panel} .yt-sf-range-value {
min-width: 34px !important;
text-align: right !important;
color: #ddd !important;
font-size: 12px !important;
font-weight: 700 !important;
}
#${IDS.panel} .yt-sf-status {
margin-top: 12px !important;
padding: 8px 10px !important;
border-radius: 10px !important;
background: rgba(255,255,255,.08) !important;
font-size: 12px !important;
color: #bbb !important;
line-height: 1.35 !important;
}
#${IDS.panel} .yt-sf-kofi {
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-top: 10px !important;
padding: 9px 12px !important;
border-radius: 12px !important;
background: ${KOFI.color} !important;
color: ${THEME.activeText} !important;
font-size: 13px !important;
font-weight: 800 !important;
text-decoration: none !important;
text-align: center !important;
}
#${IDS.panel} .yt-sf-kofi:hover {
filter: brightness(1.08) !important;
}
`;
document.head.appendChild(style);
}
function makeButton(label, action, extraClass = '') {
const button = document.createElement('button');
button.type = 'button';
button.textContent = label;
button.dataset.action = action;
if (extraClass) button.className = extraClass;
return button;
}
function makeSection(label, children, noBorder = false) {
const section = document.createElement('div');
section.className = noBorder ? 'yt-sf-section no-border' : 'yt-sf-section';
const labelEl = document.createElement('div');
labelEl.className = 'yt-sf-label';
labelEl.textContent = label;
const row = document.createElement('div');
row.className = 'yt-sf-row';
children.forEach(child => row.appendChild(child));
section.appendChild(labelEl);
section.appendChild(row);
return section;
}
function makeColorPicker(label, key, value) {
const wrap = document.createElement('label');
wrap.className = 'yt-sf-color-wrap';
const text = document.createElement('span');
text.textContent = label;
const input = document.createElement('input');
input.type = 'color';
input.value = key === 'backgroundColor' ? rgbStringToHex(value) : sanitizeHexColor(value, '#ffffff');
input.dataset.settingKey = key;
wrap.appendChild(text);
wrap.appendChild(input);
return wrap;
}
function makeOpacitySlider(value) {
const wrap = document.createElement('div');
wrap.className = 'yt-sf-range-wrap';
const label = document.createElement('span');
label.textContent = 'BG opacity';
label.style.fontSize = '12px';
label.style.fontWeight = '700';
label.style.color = '#ddd';
const input = document.createElement('input');
input.type = 'range';
input.min = '0';
input.max = '1';
input.step = '0.05';
input.value = String(value);
input.dataset.settingKey = 'backgroundOpacity';
const valueEl = document.createElement('span');
valueEl.className = 'yt-sf-range-value';
valueEl.textContent = `${Math.round(value * 100)}%`;
wrap.appendChild(label);
wrap.appendChild(input);
wrap.appendChild(valueEl);
return wrap;
}
function shouldShowSettingsButton() {
if (window.self !== window.top) return false;
if (location.pathname.startsWith('/live_chat')) return false;
if (location.pathname.startsWith('/embed')) return false;
if (location.pathname.startsWith('/watch_chat')) return false;
return true;
}
function ensureTopButton() {
if (!shouldShowSettingsButton()) {
document.getElementById(IDS.topButton)?.remove();
document.getElementById(IDS.fallbackButton)?.remove();
document.getElementById(IDS.panel)?.remove();
return;
}
injectUiStyles();
const mastheadEnd =
document.querySelector('ytd-masthead #end') ||
document.querySelector('#end.ytd-masthead') ||
document.querySelector('ytd-masthead');
let topButton = document.getElementById(IDS.topButton);
if (mastheadEnd) {
if (!topButton) {
topButton = document.createElement('div');
topButton.id = IDS.topButton;
topButton.textContent = 'YT Sub Fix';
topButton.title = 'YouTube Subtitle Fix';
topButton.setAttribute('role', 'button');
topButton.setAttribute('tabindex', '0');
mastheadEnd.insertBefore(topButton, mastheadEnd.firstChild);
}
document.getElementById(IDS.fallbackButton)?.remove();
} else {
let fallbackButton = document.getElementById(IDS.fallbackButton);
if (!fallbackButton) {
fallbackButton = document.createElement('div');
fallbackButton.id = IDS.fallbackButton;
fallbackButton.textContent = 'YT Sub Fix';
fallbackButton.title = 'YouTube Subtitle Fix';
fallbackButton.setAttribute('role', 'button');
fallbackButton.setAttribute('tabindex', '0');
document.body.appendChild(fallbackButton);
}
}
}
function toggleSettingsPanel() {
if (!shouldShowSettingsButton()) return;
const existing = document.getElementById(IDS.panel);
if (existing) {
existing.remove();
return;
}
createSettingsPanel();
}
function createSettingsPanel() {
injectUiStyles();
document.getElementById(IDS.panel)?.remove();
const panel = document.createElement('div');
panel.id = IDS.panel;
const header = document.createElement('div');
header.className = 'yt-sf-header';
const title = document.createElement('div');
title.className = 'yt-sf-title';
title.textContent = 'YouTube Subtitle Fix';
const close = makeButton('×', 'close', 'yt-sf-close');
header.appendChild(title);
header.appendChild(close);
const enabledOn = makeButton('ON', 'enabled-on', SETTINGS.enabled ? 'active' : '');
const enabledOff = makeButton('OFF', 'enabled-off', !SETTINGS.enabled ? 'active' : '');
const scopeMain = makeButton('Main video only', 'scope-main', SETTINGS.preferYouTubeSizeInSmallPlayers ? 'active' : '');
const scopeAll = makeButton('All players', 'scope-all', !SETTINGS.preferYouTubeSizeInSmallPlayers ? 'active' : '');
const modeYoutube = makeButton('YouTube size', 'mode-youtube', SETTINGS.subtitleSizeMode === 'default' ? 'active' : '');
const modeScript = makeButton('Script size', 'mode-script', SETTINGS.subtitleSizeMode === 'script' ? 'active' : '');
const modeCustom = makeButton('Custom size', 'mode-custom', SETTINGS.subtitleSizeMode === 'custom' ? 'active' : '');
const setCustomSizes = makeButton('Set custom sizes', 'set-custom-sizes');
const normalMinus = makeButton(`Normal - (${SETTINGS.customFontSizeNormal}px)`, 'normal-minus');
const normalPlus = makeButton(`Normal + (${SETTINGS.customFontSizeNormal}px)`, 'normal-plus');
const fullMinus = makeButton(`Fullscreen - (${SETTINGS.customFontSizeFullscreen}px)`, 'full-minus');
const fullPlus = makeButton(`Fullscreen + (${SETTINGS.customFontSizeFullscreen}px)`, 'full-plus');
const textColor = makeColorPicker('Text', 'textColor', SETTINGS.textColor);
const bgColor = makeColorPicker('BG', 'backgroundColor', SETTINGS.backgroundColor);
const opacitySlider = makeOpacitySlider(SETTINGS.backgroundOpacity);
const bgModeLine = makeButton('Line BG', 'bg-mode-line', SETTINGS.perLineBackground ? 'active' : '');
const bgModeBox = makeButton('Box BG', 'bg-mode-box', !SETTINGS.perLineBackground ? 'active' : '');
const wrapOn = makeButton('Wrap ON', 'wrap-on', SETTINGS.enableAutoLineBreaks ? 'active' : '');
const wrapOff = makeButton('Wrap OFF', 'wrap-off', !SETTINGS.enableAutoLineBreaks ? 'active' : '');
const lineShorter = makeButton(`Line shorter (${SETTINGS.maxCharsPerLine})`, 'line-shorter');
const lineLonger = makeButton(`Line longer (${SETTINGS.maxCharsPerLine})`, 'line-longer');
const normalUp = makeButton(`Normal up (${SETTINGS.positionNormal}px)`, 'position-normal-up');
const normalDown = makeButton(`Normal down (${SETTINGS.positionNormal}px)`, 'position-normal-down');
const fullUp = makeButton(`Fullscreen up (${SETTINGS.positionFullscreen}px)`, 'position-full-up');
const fullDown = makeButton(`Fullscreen down (${SETTINGS.positionFullscreen}px)`, 'position-full-down');
const resetPosition = makeButton('Reset position', 'position-reset');
const reset = makeButton('Reset settings', 'reset-settings', 'danger');
const status = document.createElement('div');
status.className = 'yt-sf-status';
const kofi = document.createElement('a');
kofi.className = 'yt-sf-kofi';
kofi.href = KOFI.url;
kofi.target = '_blank';
kofi.rel = 'noopener noreferrer';
kofi.textContent = KOFI.label;
panel.appendChild(header);
panel.appendChild(makeSection('Subtitle Fix', [enabledOn, enabledOff], true));
panel.appendChild(makeSection('Custom size scope', [scopeMain, scopeAll]));
panel.appendChild(makeSection('Subtitle size mode', [modeYoutube, modeScript, modeCustom]));
panel.appendChild(makeSection('Customization', [
setCustomSizes,
normalMinus,
normalPlus,
fullPlus,
fullMinus,
textColor,
bgColor,
bgModeLine,
bgModeBox,
opacitySlider
]));
panel.appendChild(makeSection('Smart line wrapping', [
wrapOn,
wrapOff,
lineShorter,
lineLonger
]));
panel.appendChild(makeSection('Subtitle position', [
normalUp,
normalDown,
fullUp,
fullDown,
resetPosition
]));
panel.appendChild(makeSection('Reset', [reset]));
panel.appendChild(status);
panel.appendChild(kofi);
document.body.appendChild(panel);
updateStatus();
}
function updateStatus() {
const status = document.querySelector(`#${IDS.panel} .yt-sf-status`);
if (!status) return;
const normalSize = SETTINGS.subtitleSizeMode === 'default' ? 'YouTube default' : `${SETTINGS.customFontSizeNormal}px`;
const fullscreenSize = SETTINGS.subtitleSizeMode === 'default' ? 'YouTube default' : `${SETTINGS.customFontSizeFullscreen}px`;
status.textContent =
`Enabled: ${SETTINGS.enabled ? 'ON' : 'OFF'} | ` +
`Size scope: ${SETTINGS.preferYouTubeSizeInSmallPlayers ? 'main only' : 'all players'} | ` +
`Mode: ${SETTINGS.subtitleSizeMode} | ` +
`Normal: ${normalSize} | ` +
`Fullscreen: ${fullscreenSize} | ` +
`Text: ${SETTINGS.textColor} | ` +
`BG: ${rgbStringToHex(SETTINGS.backgroundColor)} / ${Math.round(SETTINGS.backgroundOpacity * 100)}% | ` +
`BG mode: ${SETTINGS.perLineBackground ? 'line' : 'box'} | ` +
`Wrap: ${SETTINGS.enableAutoLineBreaks ? 'ON' : 'OFF'} | ` +
`Line: ${SETTINGS.maxCharsPerLine} chars | ` +
`Position: normal ${SETTINGS.positionNormal}px / fullscreen ${SETTINGS.positionFullscreen}px`;
}
function refreshAfterSettingChange(rebuildPanel = true) {
stylesInjected = false;
injectStyles();
scheduleProcess();
if (rebuildPanel && document.getElementById(IDS.panel)) createSettingsPanel();
else updateStatus();
}
function resetSettings() {
Object.assign(SETTINGS, DEFAULT_SETTINGS);
GM_setValue(SETTINGS_STORAGE_KEY, JSON.stringify(SETTINGS));
stylesInjected = false;
injectStyles();
scheduleProcess();
if (document.getElementById(IDS.panel)) createSettingsPanel();
notify('Subtitle settings reset.');
}
function setCustomSizesWithPrompt() {
const normalInput = promptNumber('Normal subtitle size in px:', SETTINGS.customFontSizeNormal);
if (normalInput === null) return;
const fullscreenInput = promptNumber('Fullscreen subtitle size in px:', SETTINGS.customFontSizeFullscreen);
if (fullscreenInput === null) return;
saveSettings({
customFontSizeNormal: Math.round(normalInput),
customFontSizeFullscreen: Math.round(fullscreenInput),
subtitleSizeMode: 'custom'
});
refreshAfterSettingChange(true);
}
function handlePanelAction(action) {
if (action === 'close') {
document.getElementById(IDS.panel)?.remove();
return;
}
if (action === 'enabled-on') {
saveSettings({ enabled: true });
refreshAfterSettingChange(true);
return;
}
if (action === 'enabled-off') {
saveSettings({ enabled: false });
removeSubtitleStyles();
clearCurrentCaptionInlineStyles();
refreshAfterSettingChange(true);
return;
}
if (action === 'scope-main') {
saveSettings({ preferYouTubeSizeInSmallPlayers: true });
refreshAfterSettingChange(true);
return;
}
if (action === 'scope-all') {
saveSettings({ preferYouTubeSizeInSmallPlayers: false });
refreshAfterSettingChange(true);
return;
}
if (action === 'mode-youtube') {
saveSettings({ subtitleSizeMode: 'default' });
refreshAfterSettingChange(true);
return;
}
if (action === 'mode-script') {
saveSettings({ subtitleSizeMode: 'script' });
refreshAfterSettingChange(true);
return;
}
if (action === 'mode-custom') {
saveSettings({ subtitleSizeMode: 'custom' });
refreshAfterSettingChange(true);
return;
}
if (action === 'set-custom-sizes') {
setCustomSizesWithPrompt();
return;
}
if (action === 'normal-minus') {
saveSettings({
subtitleSizeMode: 'custom',
customFontSizeNormal: Math.max(10, SETTINGS.customFontSizeNormal - 1)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'normal-plus') {
saveSettings({
subtitleSizeMode: 'custom',
customFontSizeNormal: Math.min(120, SETTINGS.customFontSizeNormal + 1)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'full-minus') {
saveSettings({
subtitleSizeMode: 'custom',
customFontSizeFullscreen: Math.max(10, SETTINGS.customFontSizeFullscreen - 1)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'full-plus') {
saveSettings({
subtitleSizeMode: 'custom',
customFontSizeFullscreen: Math.min(160, SETTINGS.customFontSizeFullscreen + 1)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'bg-mode-line') {
saveSettings({ perLineBackground: true });
refreshAfterSettingChange(true);
return;
}
if (action === 'bg-mode-box') {
saveSettings({ perLineBackground: false });
refreshAfterSettingChange(true);
return;
}
if (action === 'wrap-on') {
saveSettings({ enableAutoLineBreaks: true });
refreshAfterSettingChange(true);
return;
}
if (action === 'wrap-off') {
saveSettings({ enableAutoLineBreaks: false });
refreshAfterSettingChange(true);
return;
}
if (action === 'line-shorter') {
saveSettings({
maxCharsPerLine: clampNumber(SETTINGS.maxCharsPerLine - 1, 24, 70, DEFAULT_SETTINGS.maxCharsPerLine)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'line-longer') {
saveSettings({
maxCharsPerLine: clampNumber(SETTINGS.maxCharsPerLine + 1, 24, 70, DEFAULT_SETTINGS.maxCharsPerLine)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'position-normal-up') {
saveSettings({
positionNormal: clampNumber(SETTINGS.positionNormal + 1, -300, 300, 0)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'position-normal-down') {
saveSettings({
positionNormal: clampNumber(SETTINGS.positionNormal - 1, -300, 300, 0)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'position-full-up') {
saveSettings({
positionFullscreen: clampNumber(SETTINGS.positionFullscreen + 1, -300, 300, 0)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'position-full-down') {
saveSettings({
positionFullscreen: clampNumber(SETTINGS.positionFullscreen - 1, -300, 300, 0)
});
refreshAfterSettingChange(true);
return;
}
if (action === 'position-reset') {
saveSettings({
positionNormal: 0,
positionFullscreen: 0
});
refreshAfterSettingChange(true);
return;
}
if (action === 'reset-settings') {
resetSettings();
}
}
function handlePanelInput(event) {
const target = event.target;
if (!target || !target.dataset || !target.dataset.settingKey) return;
const key = target.dataset.settingKey;
if (key === 'textColor') {
saveSettings({
textColor: sanitizeHexColor(target.value, DEFAULT_SETTINGS.textColor)
});
refreshAfterSettingChange(false);
return;
}
if (key === 'backgroundColor') {
saveSettings({
backgroundColor: hexToRgbString(target.value)
});
refreshAfterSettingChange(false);
return;
}
if (key === 'backgroundOpacity') {
const value = clampNumber(Number(target.value), 0, 1, DEFAULT_SETTINGS.backgroundOpacity);
saveSettings({ backgroundOpacity: value });
const wrap = target.closest('.yt-sf-range-wrap');
const valueEl = wrap?.querySelector('.yt-sf-range-value');
if (valueEl) valueEl.textContent = `${Math.round(value * 100)}%`;
refreshAfterSettingChange(false);
}
}
function globalClickHandler(event) {
const panel = document.getElementById(IDS.panel);
const subFixButton =
event.target.closest(`#${IDS.topButton}`) ||
event.target.closest(`#${IDS.fallbackButton}`);
const panelButton = event.target.closest(`#${IDS.panel} button[data-action]`);
const clickedInsidePanel = panel && panel.contains(event.target);
if (panel && !clickedInsidePanel && !subFixButton) {
panel.remove();
return;
}
if (!subFixButton && !panelButton) return;
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (subFixButton) {
if (clickLock) return;
clickLock = true;
setTimeout(() => {
clickLock = false;
}, 250);
toggleSettingsPanel();
return;
}
if (panelButton) handlePanelAction(panelButton.dataset.action);
}
function isSmallPlayerContext(player) {
if (!player || isFullscreen(player)) return false;
const rect = player.getBoundingClientRect();
const width = rect.width || player.clientWidth || 0;
const height = rect.height || player.clientHeight || 0;
if (width > 0 && width <= 520) return true;
if (height > 0 && height <= 320) return true;
if (!location.pathname.startsWith('/watch')) return true;
if (player.closest('ytd-miniplayer')) return true;
if (player.closest('ytd-rich-grid-media')) return true;
if (player.closest('ytd-compact-video-renderer')) return true;
if (player.closest('ytd-video-preview')) return true;
return false;
}
function getConfiguredFontSize(full, player) {
if (SETTINGS.preferYouTubeSizeInSmallPlayers && isSmallPlayerContext(player)) return null;
if (SETTINGS.subtitleSizeMode === 'default') return null;
if (SETTINGS.subtitleSizeMode === 'custom') {
return full ? SETTINGS.customFontSizeFullscreen : SETTINGS.customFontSizeNormal;
}
return full ? SETTINGS.fontSizeFullscreen : SETTINGS.fontSizeNormal;
}
function injectStyles() {
if (!SETTINGS.enabled) {
removeSubtitleStyles();
return;
}
if (stylesInjected && document.getElementById(STYLE_ID)) return;
let style = document.getElementById(STYLE_ID);
if (!style) {
style = document.createElement('style');
style.id = STYLE_ID;
document.head.appendChild(style);
}
const normalRaise = SETTINGS.offsetNormal + SETTINGS.positionNormal;
const fullscreenRaise = SETTINGS.offsetFullscreen + SETTINGS.positionFullscreen;
const playerSelector = '#movie_player.html5-video-player';
const previewSelector = '.html5-video-player:not(#movie_player)';
style.textContent = `
${previewSelector} .caption-window {
background: ${SETTINGS.perLineBackground ? 'transparent' : `rgba(${SETTINGS.backgroundColor}, ${SETTINGS.backgroundOpacity})`} !important;
border-radius: ${SETTINGS.perLineBackground ? '0' : `${SETTINGS.borderRadius}px`} !important;
padding: ${SETTINGS.perLineBackground ? '0' : `${SETTINGS.paddingY}em ${SETTINGS.paddingX}em`} !important;
}
${previewSelector} .captions-text,
${previewSelector} .caption-visual-line,
${previewSelector} .ytp-caption-segment,
${previewSelector} .captions-text span,
${previewSelector} .caption-window span {
color: ${SETTINGS.textColor} !important;
text-shadow: ${SETTINGS.textShadow} !important;
line-height: ${SETTINGS.lineHeight} !important;
}
${previewSelector} .ytp-caption-segment,
${previewSelector} .caption-visual-line,
${previewSelector} .captions-text span,
${previewSelector} .caption-window span {
background: ${SETTINGS.perLineBackground ? `rgba(${SETTINGS.backgroundColor}, ${SETTINGS.backgroundOpacity})` : 'transparent'} !important;
padding: ${SETTINGS.perLineBackground ? `${SETTINGS.paddingY}em ${SETTINGS.paddingX}em` : '0'} !important;
border-radius: ${SETTINGS.perLineBackground ? '0' : '0'} !important;
}
${playerSelector} .ytp-caption-window-container,
${playerSelector} .caption-window,
${playerSelector} .captions-text,
${playerSelector} .caption-visual-line,
${playerSelector} .ytp-caption-segment,
${playerSelector} .captions-text span,
${playerSelector} .caption-window span {
color: ${SETTINGS.textColor} !important;
text-shadow: ${SETTINGS.textShadow} !important;
line-height: ${SETTINGS.lineHeight} !important;
}
${playerSelector} .ytp-caption-window-container {
text-align: center !important;
pointer-events: none !important;
position: absolute !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: flex-end !important;
transform: none !important;
translate: none !important;
}
${playerSelector} .caption-window {
background: ${SETTINGS.perLineBackground ? 'transparent' : `rgba(${SETTINGS.backgroundColor}, ${SETTINGS.backgroundOpacity})`} !important;
border-radius: ${SETTINGS.perLineBackground ? '0' : `${SETTINGS.borderRadius}px`} !important;
padding: ${SETTINGS.perLineBackground ? '0' : `${SETTINGS.paddingY}em ${SETTINGS.paddingX}em`} !important;
text-align: center !important;
max-width: ${SETTINGS.maxWidthPercent}% !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
margin-left: auto !important;
margin-right: auto !important;
align-self: center !important;
position: relative !important;
margin: 0 auto !important;
width: fit-content !important;
display: table !important;
transform: translateY(-${normalRaise}px) !important;
translate: none !important;
}
${playerSelector}.ytp-fullscreen .caption-window {
transform: translateY(-${fullscreenRaise}px) !important;
}
${playerSelector} .captions-text {
text-align: center !important;
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
translate: none !important;
white-space: pre-wrap !important;
word-break: normal !important;
overflow-wrap: break-word !important;
background: transparent !important;
padding: 0 !important;
border-radius: 0 !important;
}
${playerSelector} .ytp-caption-segment,
${playerSelector} .caption-visual-line,
${playerSelector} .captions-text span,
${playerSelector} .caption-window span {
left: auto !important;
right: auto !important;
top: auto !important;
bottom: auto !important;
transform: none !important;
translate: none !important;
background: transparent !important;
padding: 0 !important;
border-radius: 0 !important;
}
`;
stylesInjected = true;
}
function removeSubtitleStyles() {
document.getElementById(STYLE_ID)?.remove();
stylesInjected = false;
}
function clearCurrentCaptionInlineStyles() {
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
const captionElements = document.querySelectorAll(
'.ytp-caption-window-container, .caption-window, .captions-text, .caption-visual-line, .ytp-caption-segment, .captions-text span, .caption-window span'
);
for (const el of captionElements) {
el.removeAttribute('style');
}
}
function getPlayer() {
return document.querySelector('#movie_player.html5-video-player');
}
function getContainer(player) {
return player?.querySelector('.ytp-caption-window-container') || null;
}
function getCaption(player) {
if (!player) return null;
hideCaptionInfoWindows(player);
return Array.from(player.querySelectorAll('.caption-window'))
.find(caption => !isCaptionInfoWindow(caption)) || null;
}
function getCaptionsText(caption) {
return caption?.querySelector('.captions-text') || null;
}
function getCaptionText(caption) {
return String(caption?.innerText || caption?.textContent || '').replace(/\s+/g, ' ').trim();
}
function isCaptionInfoWindow(caption) {
const text = getCaptionText(caption).toLowerCase();
if (!text) return false;
if (text.includes('click for settings') || text.includes('for settings')) return true;
return (
text.includes('auto-generated') && (
text.includes('click') ||
text.includes('settings') ||
text.includes('for settings')
)
);
}
function hideCaptionInfoWindows(player) {
if (!player) return;
const captions = player.querySelectorAll('.caption-window');
for (const caption of captions) {
if (!isCaptionInfoWindow(caption)) continue;
caption.dataset.ytSubFixHiddenInfo = 'true';
caption.style.setProperty('display', 'none', 'important');
caption.style.setProperty('visibility', 'hidden', 'important');
caption.setAttribute('aria-hidden', 'true');
}
}
function isFullscreen(player) {
return !!player?.classList.contains('ytp-fullscreen');
}
function isAutoGeneratedCaptionActive() {
const video = document.querySelector('video');
const tracks = Array.from(video?.textTracks || []);
return tracks.some(track => {
const label = `${track.label || ''} ${track.language || ''}`.toLowerCase();
return track.mode === 'showing' && /auto-generated|automatic|asr/.test(label);
});
}
function countVisualLines(caption) {
if (!caption) return 1;
const textNode = getCaptionsText(caption);
if (textNode) {
const txt = (textNode.innerText || '').trim();
if (txt) {
const lines = txt.split('\n').map(v => v.trim()).filter(Boolean);
if (lines.length > 0) return lines.length;
}
}
const visualLines = caption.querySelectorAll('.caption-visual-line');
if (visualLines.length > 0) return visualLines.length;
const segs = caption.querySelectorAll('.ytp-caption-segment');
return Math.max(segs.length, 1);
}
function computeRaise(full, lineCount) {
const base = full ? SETTINGS.offsetFullscreen : SETTINGS.offsetNormal;
const position = full ? SETTINGS.positionFullscreen : SETTINGS.positionNormal;
return base + position + (lineCount - 1) * SETTINGS.extraPerLine;
}
function joinWords(words, start, end) {
return words.slice(start, end).join(' ').trim();
}
function tokenizePreservingNotes(line) {
return line.match(/\[[^\]]+\]|\S+/g) || [];
}
function isTranslatorNoteLine(line) {
return /^\[[\s\S]+\]$/.test(String(line || '').trim());
}
function isWeakLineStart(word) {
return /^(and|or|but|so|yet|for|nor|to|of|in|on|at|by|with|from|into|onto|than|that|who|which|because)$/i.test(word);
}
function isWeakLineEnd(word) {
return /^(a|an|the|and|or|but|so|yet|for|nor|to|of|in|on|at|by|with|from|into|onto|than|that)$/i.test(word);
}
function getLinePenalty(text, softMax) {
if (!text) return 100000;
const words = text.split(/\s+/).filter(Boolean);
const firstWord = words[0] || '';
const lastWord = words[words.length - 1] || '';
let penalty = Math.abs(text.length - softMax) * 0.35;
if (text.length > softMax) penalty += (text.length - softMax) * 1.6;
if (text.length < Math.max(10, Math.floor(softMax * 0.4))) penalty += 25;
if (isWeakLineStart(firstWord)) penalty += 18;
if (isWeakLineEnd(lastWord)) penalty += 12;
if (/^[)\],.!?:;]/.test(firstWord)) penalty += 30;
if (/[(\[{'"-]$/.test(lastWord)) penalty += 18;
return penalty;
}
function chooseBreaks(words, lineCount, softMax) {
const totalWords = words.length;
const memo = new Map();
function solve(startIndex, linesLeft) {
const key = `${startIndex}:${linesLeft}`;
if (memo.has(key)) return memo.get(key);
if (linesLeft === 1) {
const text = joinWords(words, startIndex, totalWords);
const result = {
score: getLinePenalty(text, softMax),
lines: [text]
};
memo.set(key, result);
return result;
}
let best = null;
const minBreak = startIndex + 1;
const maxBreak = totalWords - (linesLeft - 1);
for (let breakIndex = minBreak; breakIndex <= maxBreak; breakIndex++) {
const current = joinWords(words, startIndex, breakIndex);
const remaining = solve(breakIndex, linesLeft - 1);
if (!current || !remaining.lines.length) continue;
const lengths = [current.length, ...remaining.lines.map(line => line.length)];
const balancePenalty = Math.max(...lengths) - Math.min(...lengths);
const score = getLinePenalty(current, softMax) + remaining.score + balancePenalty * 0.75;
if (!best || score < best.score) {
best = {
score,
lines: [current, ...remaining.lines]
};
}
}
const fallback = best || {
score: 100000,
lines: [joinWords(words, startIndex, totalWords)]
};
memo.set(key, fallback);
return fallback;
}
return solve(0, lineCount).lines.filter(Boolean);
}
function wrapLine(line, maxChars) {
const trimmed = line.trim().replace(/\s+/g, ' ');
if (!trimmed || trimmed.length <= maxChars) return [trimmed];
const words = tokenizePreservingNotes(trimmed);
if (words.length < 2) return [trimmed];
const softTwoLineLimit = Math.round(maxChars * 2.35);
const preferredLines = trimmed.length <= softTwoLineLimit ? 2 : Math.min(3, Math.max(2, Math.ceil(trimmed.length / maxChars)));
const wrapped = chooseBreaks(words, preferredLines, maxChars);
return wrapped.length ? wrapped : [trimmed];
}
function buildWrappedText(text) {
const maxChars = SETTINGS.maxCharsPerLine;
const sourceLines = String(text || '')
.replace(/\r/g, '')
.split('\n')
.map(v => v.trim())
.filter(Boolean);
if (!sourceLines.length) return '';
const hasStandaloneTranslatorNote = sourceLines.some(isTranslatorNoteLine);
const preserveExistingTwoLineLayout = hasStandaloneTranslatorNote && sourceLines.length >= 2;
return sourceLines
.flatMap(line => {
if (!SETTINGS.enableAutoLineBreaks) return [line];
if (preserveExistingTwoLineLayout) return [line];
return wrapLine(line, maxChars);
})
.join('\n');
}
function getDisplayText(caption) {
const textNode = getCaptionsText(caption);
if (!textNode) return '';
const visualLines = Array.from(caption.querySelectorAll('.caption-visual-line'))
.map(line => (line.innerText || '').trim())
.filter(Boolean);
if (visualLines.length > 0) {
return buildWrappedText(visualLines.join('\n'));
}
return buildWrappedText(textNode.innerText || textNode.textContent || '');
}
function renderPerLineBackgroundText(textNode, text) {
const lines = String(text || '')
.split('\n')
.map(line => line.trim())
.filter(Boolean);
const renderKey = lines.join('\n');
if (!renderKey) return;
if (textNode.dataset.ytSubFixRenderedText === renderKey && textNode.querySelector(`.${LINE_BACKGROUND_CLASS}`)) {
return;
}
textNode.replaceChildren();
lines.forEach((line, index) => {
if (index > 0) textNode.appendChild(document.createElement('br'));
const lineEl = document.createElement('span');
lineEl.className = LINE_BACKGROUND_CLASS;
lineEl.textContent = line;
textNode.appendChild(lineEl);
});
textNode.dataset.ytSubFixRenderedText = renderKey;
}
function applyContainerStyles(container) {
container.style.position = 'absolute';
container.style.left = '0';
container.style.right = '0';
container.style.width = '100%';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.justifyContent = 'center';
container.style.alignItems = 'flex-end';
container.style.pointerEvents = 'none';
container.style.textAlign = 'center';
container.style.bottom = '0';
container.style.margin = '0';
container.style.padding = '0';
container.style.transform = 'none';
container.style.translate = 'none';
container.style.setProperty('align-items', 'center', 'important');
container.style.setProperty('justify-content', 'flex-end', 'important');
container.style.setProperty('transform', 'none', 'important');
container.style.setProperty('translate', 'none', 'important');
}
function applyCaptionStyles(caption, raisePx) {
caption.style.position = 'relative';
caption.style.setProperty('left', 'auto', 'important');
caption.style.setProperty('right', 'auto', 'important');
caption.style.setProperty('top', 'auto', 'important');
caption.style.setProperty('bottom', 'auto', 'important');
caption.style.margin = '0 auto';
caption.style.textAlign = 'center';
caption.style.alignSelf = 'center';
caption.style.width = 'fit-content';
caption.style.maxWidth = `${SETTINGS.maxWidthPercent}%`;
caption.style.display = 'table';
caption.style.transform = `translateY(-${raisePx}px)`;
caption.style.translate = 'none';
caption.style.setProperty('background', SETTINGS.perLineBackground ? 'transparent' : `rgba(${SETTINGS.backgroundColor}, ${SETTINGS.backgroundOpacity})`, 'important');
caption.style.setProperty('padding', SETTINGS.perLineBackground ? '0' : `${SETTINGS.paddingY}em ${SETTINGS.paddingX}em`, 'important');
caption.style.setProperty('border-radius', SETTINGS.perLineBackground ? '0' : `${SETTINGS.borderRadius}px`, 'important');
}
function applyTextStyles(caption, full) {
const fontSize = getConfiguredFontSize(full, getPlayer());
const textNode = getCaptionsText(caption);
const preserveOriginalText = SETTINGS.preserveAutoGeneratedCaptions && isAutoGeneratedCaptionActive();
if (textNode) {
if (!preserveOriginalText) {
const displayText = getDisplayText(caption);
if (displayText && SETTINGS.perLineBackground) {
renderPerLineBackgroundText(textNode, displayText);
} else if (displayText && textNode.textContent !== displayText) {
textNode.textContent = displayText;
delete textNode.dataset.ytSubFixRenderedText;
}
} else if (textNode.dataset.ytSubFixRenderedText) {
textNode.textContent = textNode.innerText || textNode.textContent || '';
delete textNode.dataset.ytSubFixRenderedText;
}
textNode.style.display = 'block';
textNode.style.textAlign = 'center';
textNode.style.setProperty('left', 'auto', 'important');
textNode.style.setProperty('right', 'auto', 'important');
textNode.style.setProperty('top', 'auto', 'important');
textNode.style.setProperty('bottom', 'auto', 'important');
textNode.style.setProperty('transform', 'none', 'important');
textNode.style.setProperty('translate', 'none', 'important');
textNode.style.whiteSpace = 'pre-wrap';
textNode.style.wordBreak = 'normal';
textNode.style.overflowWrap = 'break-word';
if (fontSize === null) {
textNode.style.removeProperty('font-size');
} else {
textNode.style.fontSize = `${fontSize}px`;
}
textNode.style.lineHeight = String(SETTINGS.lineHeight);
textNode.style.color = SETTINGS.textColor;
textNode.style.textShadow = SETTINGS.textShadow;
textNode.style.setProperty('background', 'transparent', 'important');
textNode.style.setProperty('padding', '0', 'important');
textNode.style.setProperty('border-radius', '0', 'important');
textNode.style.margin = '0 auto';
}
const all = caption.querySelectorAll('.ytp-caption-segment, .caption-visual-line, .captions-text span, .caption-window span');
for (const el of all) {
if (el === textNode) continue;
if (fontSize === null) {
el.style.removeProperty('font-size');
} else {
el.style.fontSize = `${fontSize}px`;
}
el.style.lineHeight = String(SETTINGS.lineHeight);
el.style.color = SETTINGS.textColor;
el.style.textShadow = SETTINGS.textShadow;
el.style.textAlign = 'center';
if (el.classList.contains(LINE_BACKGROUND_CLASS)) {
el.style.setProperty('display', 'inline-block', 'important');
el.style.setProperty('background', `rgba(${SETTINGS.backgroundColor}, ${SETTINGS.backgroundOpacity})`, 'important');
el.style.setProperty('padding', `${SETTINGS.paddingY}em ${SETTINGS.paddingX}em`, 'important');
el.style.setProperty('border-radius', '0', 'important');
el.style.setProperty('margin', '0 auto', 'important');
continue;
}
el.style.setProperty('left', 'auto', 'important');
el.style.setProperty('right', 'auto', 'important');
el.style.setProperty('top', 'auto', 'important');
el.style.setProperty('bottom', 'auto', 'important');
el.style.setProperty('transform', 'none', 'important');
el.style.setProperty('translate', 'none', 'important');
el.style.background = 'transparent';
el.style.padding = '0';
el.style.borderRadius = '0';
}
}
function processSubtitles() {
if (!SETTINGS.enabled) return;
const player = getPlayer();
if (!player) return;
hideCaptionInfoWindows(player);
const container = getContainer(player);
const caption = getCaption(player);
if (!container || !caption) return;
observersPaused = true;
if (observerResumeId) clearTimeout(observerResumeId);
const full = isFullscreen(player);
applyTextStyles(caption, full);
const lineCount = countVisualLines(caption);
const raisePx = computeRaise(full, lineCount);
applyContainerStyles(container);
applyCaptionStyles(caption, raisePx);
observerResumeId = setTimeout(() => {
observersPaused = false;
observerResumeId = null;
}, 0);
}
function scheduleProcess() {
if (observersPaused) return;
processSubtitles();
if (rafId) cancelAnimationFrame(rafId);
rafId = requestAnimationFrame(() => {
rafId = null;
processSubtitles();
});
}
function attachCaptionObserver() {
const player = getPlayer();
if (!player) return;
const container = getContainer(player);
if (!container) return;
if (captionObserver) captionObserver.disconnect();
captionObserver = new MutationObserver(() => {
if (observersPaused) return;
scheduleProcess();
});
captionObserver.observe(container, {
childList: true,
subtree: true,
characterData: true
});
scheduleProcess();
}
function init() {
ensureTopButton();
injectStyles();
const player = getPlayer();
if (!player) return false;
if (playerObserver) playerObserver.disconnect();
playerObserver = new MutationObserver(() => {
if (observersPaused) return;
attachCaptionObserver();
scheduleProcess();
});
playerObserver.observe(player, {
childList: true,
subtree: true,
attributes: false
});
attachCaptionObserver();
scheduleProcess();
return true;
}
function hookEvents() {
document.addEventListener('click', globalClickHandler, true);
document.addEventListener('pointerdown', globalClickHandler, true);
document.addEventListener('input', handlePanelInput, true);
document.addEventListener('change', handlePanelInput, true);
document.addEventListener('fullscreenchange', () => {
scheduleProcess();
setTimeout(scheduleProcess, 150);
});
window.addEventListener('yt-navigate-finish', () => {
stylesInjected = false;
injectStyles();
setTimeout(ensureTopButton, 200);
setTimeout(init, 200);
setTimeout(scheduleProcess, 500);
});
setInterval(() => {
ensureTopButton();
updateStatus();
}, 1500);
}
injectUiStyles();
ensureTopButton();
injectStyles();
hookEvents();
const wait = setInterval(() => {
if (init()) clearInterval(wait);
}, 500);
})();