// ==UserScript==
// @name Youtube Enhancer By Domopremo
// @namespace DomopremoScripts
// @version 1.0.0
// @description Adds an ergonomic right-side tab panel (Info / Comments / Videos / Transcript), fast speed controls, ad-marking, keyboard shortcuts, last-tab+scroll restore, and a quick settings UI for YouTube. Stable, performant, and accessible.
// @author Domopremo
// @license MIT
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @icon https://www.youtube.com/s/desktop/5e8b1b8a/img/favicon_144x144.png
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_openInTab
// @run-at document-start
// ==/UserScript==
(() => {
'use strict';
/********************************************************************
* Minimal utilities
********************************************************************/
const onReady = (fn) => {
if (document.readyState !== 'loading') fn();
else document.addEventListener('DOMContentLoaded', fn, { once: true });
};
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const SKEY = {
SETTINGS: 'de/settings/v1',
LAST: 'de/lastTabState', // { tabId, scroll: { "#tab-info": n, ... }, videoId }
SPEED_GLOBAL: 'de/speed/global',
SPEED_BY_CHANNEL: (channelId) => `de/speed/ch/${channelId}`
};
const defaults = {
ui: {
compactHeader: false,
theme: 'system', // 'system' | 'light' | 'dark'
rounded: 12
},
features: {
speedControl: true,
markAds: true,
autoExpandDesc: true,
transcriptTab: true,
lastTabRestore: true,
scrollRestore: true,
rememberSpeedByChannel: false
},
shortcuts: {
goInfo: 'g i',
goComments: 'g c',
goVideos: 'g v',
goTranscript: 'g t',
screenshot: 'ctrl+shift+s',
focusTabs: 'g g'
}
};
const store = {
get() {
try { return JSON.parse(GM_getValue(SKEY.SETTINGS, '')) || JSON.parse(JSON.stringify(defaults)); }
catch { return JSON.parse(JSON.stringify(defaults)); }
},
set(val) { GM_setValue(SKEY.SETTINGS, JSON.stringify(val)); }
};
const state = {
settings: store.get(),
currentTab: '',
scrollCache: { '#tab-info': 0, '#tab-comments': 0, '#tab-videos': 0, '#tab-transcript': 0 },
channelId: null,
videoId: null,
};
const SEL = {
flexy: ['ytd-watch-flexy[flexy]', 'ytd-watch-flexy'],
secondaryInner: ['#secondary-inner.style-scope.ytd-watch-flexy'],
related: ['#related.ytd-watch-flexy', '#related'],
comments: ['#comments'],
infoBlock: ['ytd-expandable-video-description-body-renderer', 'ytd-expander#expander'],
rightControls: ['.ytp-right-controls'],
player: ['#movie_player'],
sizeBtn: ['#ytd-player .ytp-size-button', '.ytp-size-button'],
ytcApp: ['ytd-app'],
playlistPanel: ['ytd-playlist-panel-renderer#playlist'],
transcriptPanel: ['ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-searchable-transcript"]'],
};
const $first = (candidates) => {
for (const sel of candidates) { const el = document.querySelector(sel); if (el) return el; }
return null;
};
/********************************************************************
* Styles (compact, theme-aware, accessible tabs)
********************************************************************/
GM_addStyle(`
:root {
--de-rounded: ${state.settings.ui.rounded}px;
}
#de-right-tabs { display:flex; flex-direction:column; gap:0; border:1px solid var(--ytd-searchbox-legacy-border-color); border-radius: var(--de-rounded); overflow:hidden; }
#de-tabs-bar { display:flex; align-items:stretch; border-bottom:1px solid var(--ytd-searchbox-legacy-border-color); }
#de-tabs-bar button[role="tab"]{
flex:1 1 0;
padding:${state.settings.ui.compactHeader ? '8px 6px' : '12px 10px'};
background: var(--ytd-searchbox-legacy-button-color);
color: var(--yt-spec-text-secondary);
border:0; border-right:1px solid var(--ytd-searchbox-legacy-border-color);
text-transform:var(--yt-button-text-transform, none);
cursor:pointer; outline: none;
}
#de-tabs-bar button[role="tab"]:last-child{ border-right:0; }
#de-tabs-bar button[aria-selected="true"]{
background: var(--ytd-searchbox-legacy-button-focus-color);
color: var(--yt-spec-text-primary);
box-shadow: inset 0 -2px var(--yt-brand-light-red);
}
#de-tabs-body { position:relative; height:100%; }
.de-tabpane { display:none; padding: var(--ytd-margin-4x); }
.de-tabpane[aria-hidden="false"]{ display:block; }
#de-speed-btn {
width: 3.5em; text-align:center; font-size:12px; border-radius:8px; cursor:pointer;
}
#de-speed-menu{
position:absolute; bottom:calc(100% + 10px); right:0; background:#303031; color:#fff;
border-radius:6px; display:none; min-width:56px; z-index:99999;
}
#de-speed-menu .opt{ padding:6px 8px; cursor:pointer; text-align:center; }
#de-speed-menu .opt.active, #de-speed-menu .opt:hover{ font-weight:600; }
#de-speed-toast{
position:absolute; inset:0; margin:auto; width:80px; height:80px; line-height:80px;
background:#303031; color:#f3f3f3; font-size:30px; border-radius:20px; text-align:center;
opacity:.9; display:none; z-index:9999999;
}
.de-gear { margin-left:auto; padding:0 8px; cursor:pointer; }
.de-settings {
position:fixed; inset:auto 16px 16px auto; width:320px; max-width:90vw;
background:var(--yt-spec-brand-background-primary); color:var(--yt-spec-text-primary);
border:1px solid var(--ytd-searchbox-legacy-border-color); border-radius:12px; padding:12px;
box-shadow:0 10px 24px rgba(0,0,0,.3); z-index:999999;
display:none;
}
.de-settings h3{ margin:0 0 8px; font-size:16px; }
.de-row{ display:flex; align-items:center; justify-content:space-between; gap:8px; margin:8px 0; }
.de-row label{ font-size:13px; }
.de-shortcut{ width:140px; }
[data-de-hidden="true"]{ display:none !important; }
`);
/********************************************************************
* Core DOM build
********************************************************************/
const Tabs = (() => {
const ids = ['#tab-info', '#tab-comments', '#tab-videos', '#tab-transcript'];
const pretty = { '#tab-info':'Info', '#tab-comments':'Comments', '#tab-videos':'Videos', '#tab-transcript':'Transcript' };
function buildContainer() {
const wrap = document.createElement('div');
wrap.id = 'de-right-tabs';
const bar = document.createElement('div');
bar.id = 'de-tabs-bar';
bar.setAttribute('role','tablist');
wrap.append(bar);
const gear = document.createElement('button');
gear.type='button'; gear.className='de-gear'; gear.title='Settings'; gear.textContent='⚙︎';
gear.addEventListener('click', Settings.toggle);
bar.append(gear);
const body = document.createElement('div');
body.id = 'de-tabs-body';
wrap.append(body);
// panes
for (const id of ids) {
const pane = document.createElement('div');
pane.id = id.slice(1);
pane.className = 'de-tabpane';
pane.setAttribute('role','tabpanel');
pane.setAttribute('aria-hidden','true');
body.append(pane);
}
// tabs (insert before gear)
for (const id of ids) {
if (id === '#tab-transcript' && !state.settings.features.transcriptTab) continue;
const btn = document.createElement('button');
btn.type='button';
btn.setAttribute('role','tab');
btn.setAttribute('aria-controls', id.slice(1));
btn.dataset.deTabTarget = id;
btn.textContent = pretty[id];
btn.addEventListener('click', () => switchTo(id));
bar.insertBefore(btn, gear);
}
return wrap;
}
function switchTo(id) {
// Save previous scroll
if (state.currentTab && state.settings.features.scrollRestore) {
const oldPane = document.querySelector(state.currentTab);
if (oldPane) state.scrollCache[state.currentTab] = oldPane.scrollTop || 0;
}
// Update pane visibility
for (const pane of document.querySelectorAll('.de-tabpane')) {
const active = `#${pane.id}` === id;
pane.setAttribute('aria-hidden', String(!active));
pane.hidden = !active;
if (active && state.settings.features.scrollRestore) {
const sc = state.scrollCache[id] || 0;
pane.scrollTop = sc;
}
}
// Update tabs aria
for (const btn of document.querySelectorAll('#de-tabs-bar [role="tab"]')) {
btn.setAttribute('aria-selected', String(btn.dataset.deTabTarget === id));
}
state.currentTab = id;
if (state.settings.features.lastTabRestore) persistLastTab();
}
function ensureInSecondary(container) {
const secondaryInner = $first(SEL.secondaryInner);
if (!secondaryInner) return false;
// Wrap in absolute column container (no layout shift in 2-column mode)
container.style.marginTop = 'var(--ytd-margin-3x)';
if (!secondaryInner.querySelector('#de-right-tabs')) {
// Ensure the wrapper container for proper height behavior
let wrapper = secondaryInner.querySelector('secondary-wrapper');
if (!wrapper) {
wrapper = document.createElement('secondary-wrapper');
// Move all children into wrapper to keep YouTube logic intact
while (secondaryInner.firstChild) wrapper.appendChild(secondaryInner.firstChild);
secondaryInner.appendChild(wrapper);
}
// Place our tabs at the top
wrapper.insertBefore(container, wrapper.firstChild);
}
return true;
}
function mount() {
if (document.getElementById('de-right-tabs')) return true;
const box = buildContainer();
return ensureInSecondary(box);
}
function persistLastTab() {
const payload = {
tabId: state.currentTab,
scroll: state.settings.features.scrollRestore ? state.scrollCache : {},
videoId: state.videoId || null
};
GM_setValue(SKEY.LAST, JSON.stringify(payload));
}
function restoreLastTab() {
if (!state.settings.features.lastTabRestore) return;
try {
const raw = GM_getValue(SKEY.LAST, '');
if (!raw) return switchTo('#tab-videos');
const obj = JSON.parse(raw);
// If video changed, prefer Info tab; else restore last tab
const id = (obj && obj.videoId === state.videoId && obj.tabId) ? obj.tabId : '#tab-videos';
state.scrollCache = obj.scroll || state.scrollCache;
switchTo(id);
} catch {
switchTo('#tab-videos');
}
}
return { mount, switchTo, restoreLastTab };
})();
/********************************************************************
* Settings panel + GM menu
********************************************************************/
const Settings = (() => {
let panel = null;
function open() {
if (!panel) build();
panel.style.display = 'block';
}
function close() {
if (panel) panel.style.display = 'none';
}
function toggle() { (panel && panel.style.display === 'block') ? close() : open(); }
function saverefresh() {
store.set(state.settings);
}
function build() {
panel = document.createElement('div');
panel.className = 'de-settings';
panel.innerHTML = `
<h3>Youtube Enhancer – Settings</h3>
<div class="de-row">
<label><input type="checkbox" data-k="features.speedControl"> Speed control</label>
</div>
<div class="de-row">
<label><input type="checkbox" data-k="features.markAds"> Mark ads</label>
</div>
<div class="de-row">
<label><input type="checkbox" data-k="features.autoExpandDesc"> Auto-expand description</label>
</div>
<div class="de-row">
<label><input type="checkbox" data-k="features.transcriptTab"> Transcript tab</label>
</div>
<div class="de-row">
<label><input type="checkbox" data-k="features.lastTabRestore"> Remember last tab</label>
</div>
<div class="de-row">
<label><input type="checkbox" data-k="features.scrollRestore"> Restore scroll per tab</label>
</div>
<div class="de-row">
<label>Theme</label>
<select data-k="ui.theme">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div class="de-row">
<label>Rounded (px)</label>
<input type="number" min="0" max="32" data-k="ui.rounded" style="width:64px">
</div>
<hr>
<div><strong>Shortcuts</strong></div>
<div class="de-row"><label>Go Info</label><input class="de-shortcut" data-k="shortcuts.goInfo"></div>
<div class="de-row"><label>Go Comments</label><input class="de-shortcut" data-k="shortcuts.goComments"></div>
<div class="de-row"><label>Go Videos</label><input class="de-shortcut" data-k="shortcuts.goVideos"></div>
<div class="de-row"><label>Go Transcript</label><input class="de-shortcut" data-k="shortcuts.goTranscript"></div>
<div class="de-row"><label>Screenshot</label><input class="de-shortcut" data-k="shortcuts.screenshot"></div>
<div class="de-row"><label>Focus tabs</label><input class="de-shortcut" data-k="shortcuts.focusTabs"></div>
<div style="text-align:right; margin-top:8px;">
<button id="de-settings-close">Close</button>
</div>
`;
document.body.append(panel);
// bind
panel.querySelector('#de-settings-close').addEventListener('click', close);
for (const input of panel.querySelectorAll('[data-k]')) {
const path = input.dataset.k.split('.');
// set initial
let ref = state.settings;
for (let i=0;i<path.length-1;i++) ref = ref[path[i]];
const key = path[path.length-1];
if (input.type === 'checkbox') input.checked = !!ref[key];
else input.value = ref[key];
input.addEventListener('change', () => {
let r = state.settings;
for (let i=0;i<path.length-1;i++) r = r[path[i]];
r[key] = (input.type === 'checkbox') ? input.checked : (input.classList.contains('de-shortcut') ? input.value.trim() : (input.type==='number' ? Number(input.value) : input.value));
saverefresh();
if (key === 'rounded') document.documentElement.style.setProperty('--de-rounded', `${state.settings.ui.rounded}px`);
});
}
}
GM_registerMenuCommand('Open settings', open);
return { open, close, toggle };
})();
/********************************************************************
* Speed control
********************************************************************/
const Speed = (() => {
let btn, menu, toast, current = Number(GM_getValue(SKEY.SPEED_GLOBAL, 1)) || 1;
const list = [0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 2.5, 3];
function mount() {
if (!state.settings.features.speedControl) return;
const rc = $first(SEL.rightControls);
if (!rc || rc.querySelector('#de-speed-btn')) return;
btn = document.createElement('div'); btn.id = 'de-speed-btn'; btn.className = 'ytp-button';
btn.textContent = `${current.toFixed(2).replace(/\.00$/,'')}×`;
btn.style.position='relative';
menu = document.createElement('div'); menu.id = 'de-speed-menu';
for (const v of list) {
const opt = document.createElement('div');
opt.className = 'opt' + (v===current?' active':'');
opt.textContent = `${v}x`;
opt.dataset.v = String(v);
opt.addEventListener('click', () => setRate(v, true));
menu.append(opt);
}
btn.append(menu);
btn.addEventListener('mouseenter', ()=> menu.style.display='block');
btn.addEventListener('mouseleave', ()=> menu.style.display='none');
toast = document.createElement('div'); toast.id = 'de-speed-toast';
const player = $first(SEL.player) || document.body;
player.appendChild(toast);
rc.prepend(btn);
applyToCurrentVideo(current);
}
function setRate(v, persist=false) {
current = v;
btn && (btn.firstChild.nodeType===3) && (btn.firstChild.nodeValue = `${v}×`);
for (const x of menu.querySelectorAll('.opt')) x.classList.toggle('active', Number(x.dataset.v)===v);
showToast(`${v}×`);
applyToCurrentVideo(v);
if (persist) {
if (state.settings.features.rememberSpeedByChannel && state.channelId) {
GM_setValue(SKEY.SPEED_BY_CHANNEL(state.channelId), String(v));
} else {
GM_setValue(SKEY.SPEED_GLOBAL, String(v));
}
}
}
function showToast(text) {
if (!toast) return;
toast.textContent = text;
toast.style.display='block';
toast.style.opacity='0.9';
requestAnimationFrame(()=>{
setTimeout(()=> toast && (toast.style.display='none'), 1200);
});
}
function applyToCurrentVideo(v) {
const set = () => {
const vid = document.querySelector('video');
if (vid) vid.playbackRate = v;
};
set();
// If YT swaps src, keep rate
const iv = setInterval(()=>{
const vid = document.querySelector('video');
if (!vid) return;
clearInterval(iv);
const mo = new MutationObserver((m)=>{
for (const mu of m) if (mu.attributeName==='src') set();
});
mo.observe(vid, { attributes:true });
}, 1000);
}
function loadPreferred() {
if (state.settings.features.rememberSpeedByChannel && state.channelId) {
const ch = Number(GM_getValue(SKEY.SPEED_BY_CHANNEL(state.channelId), current));
current = ch || current;
} else {
current = Number(GM_getValue(SKEY.SPEED_GLOBAL, current)) || current;
}
}
return { mount, setRate, loadPreferred };
})();
/********************************************************************
* Keyboard shortcuts
********************************************************************/
const Shortcuts = (() => {
const parseCombo = (s) => s.trim().toLowerCase();
const matches = (evt, combo) => {
if (!combo) return false;
const parts = combo.split('+').map(x=>x.trim());
const needCtrl = parts.includes('ctrl');
const needShift = parts.includes('shift');
const needAlt = parts.includes('alt');
const key = parts[parts.length-1];
const pressedKey = evt.key?.toLowerCase();
return (!!evt.ctrlKey===needCtrl) && (!!evt.shiftKey===needShift) && (!!evt.altKey===needAlt) && (pressedKey===key);
};
let glueMode = false; // for "g i" style combos
let glueTimer = 0;
function onKeydown(evt) {
const sc = state.settings.shortcuts;
// single combos
if (matches(evt, parseCombo(sc.screenshot))) { evt.preventDefault(); Screenshot.capture(); return; }
if (matches(evt, parseCombo(sc.focusTabs))) {
evt.preventDefault();
document.querySelector('#de-tabs-bar [role="tab"]')?.focus();
return;
}
// leader "g"
if (evt.key.toLowerCase()==='g' && !evt.ctrlKey && !evt.shiftKey && !evt.altKey) {
glueMode = true;
clearTimeout(glueTimer);
glueTimer = setTimeout(()=> glueMode=false, 850);
return;
}
if (glueMode) {
if (evt.key.toLowerCase()==='i') { glueMode=false; Tabs.switchTo('#tab-info'); }
else if (evt.key.toLowerCase()==='c') { glueMode=false; Tabs.switchTo('#tab-comments'); }
else if (evt.key.toLowerCase()==='v') { glueMode=false; Tabs.switchTo('#tab-videos'); }
else if (evt.key.toLowerCase()==='t') { glueMode=false; Tabs.switchTo('#tab-transcript'); }
}
}
function init() {
window.addEventListener('keydown', onKeydown, true);
}
return { init };
})();
/********************************************************************
* Screenshot
********************************************************************/
const Screenshot = (() => {
function sanitize(name) {
return name.replace(/[\\/:*?"<>|]+/g,' ').slice(0,150).trim();
}
function titleStamp() {
const h1 = document.querySelector('h1.title') || document.querySelector('h1.ytd-watch-metadata');
const t = h1 ? (h1.textContent||'').trim() : 'YouTube';
const vid = document.querySelector('video');
let stamp = '0-00';
if (vid) {
const ct = Math.floor(vid.currentTime||0);
const m = Math.floor(ct/60), s = ct%60;
stamp = `${m}-${s.toString().padStart(2,'0')}`;
}
return `${sanitize(t)} ${stamp} screenshot.png`;
}
function capture() {
const v = document.querySelector('video');
if (!v) return;
const canvas = document.createElement('canvas');
canvas.width = v.videoWidth; canvas.height = v.videoHeight;
canvas.getContext('2d').drawImage(v,0,0,canvas.width,canvas.height);
canvas.toBlob((blob)=>{
const a = document.createElement('a');
a.download = titleStamp();
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href);
}, 'image/png');
}
return { capture };
})();
/********************************************************************
* Ad marking (visual only, low risk)
********************************************************************/
const Ads = (() => {
const CSS = `
#masthead-ad, ytd-ad-slot-renderer, ytd-display-ad-renderer, .video-ads.ytp-ad-module,
ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-ads"],
#related #player-ads, #related ytd-ad-slot-renderer, ad-slot-renderer, ytm-companion-ad-renderer {
outline: 2px dashed #f66 !important; filter: saturate(.6) brightness(.95);
}
`;
let added = false;
function activate() {
if (added || !state.settings.features.markAds) return;
GM_addStyle(CSS); added = true;
}
return { activate };
})();
/********************************************************************
* Transcript Tab (toggle engagement panel)
********************************************************************/
const Transcript = (() => {
function openPanel(show=true) {
// show/hide via YT actions by clicking the transcript button if present
const btn = document.querySelector('button[aria-label*="Transcript"], ytd-menu-service-item-download-renderer[is-transcript] button');
if (btn) { btn.click(); return; }
// fallback: try to toggle any expanded panel except ours
// (kept simple to avoid fragile deep calls)
}
function ensureTabVisibility() {
const tab = document.querySelector('#de-tabs-bar [data-de-tab-target="#tab-transcript"]')?.closest('[role="tab"]');
if (!tab) return;
tab.parentElement?.parentElement?.querySelector('#tab-transcript')
?.setAttribute('data-de-hidden', String(!state.settings.features.transcriptTab));
tab.setAttribute('data-de-hidden', String(!state.settings.features.transcriptTab));
}
return { openPanel, ensureTabVisibility };
})();
/********************************************************************
* Observers: wire up Info / Comments / Videos into tabs, auto-expand desc
********************************************************************/
const WireUp = (() => {
function moveIfPresent(selList, targetPaneSelector) {
const pane = document.querySelector(targetPaneSelector);
if (!pane) return;
const el = $first(selList);
if (el && !pane.contains(el)) {
pane.append(el);
}
}
async function run() {
// Wait flexy
for (let i=0; i<60; i++){
const flexy = $first(SEL.flexy);
if (flexy) break; // ok
await sleep(250);
}
// Build right tabs and mount
Tabs.mount();
// Move known blocks when they appear
const mo = new MutationObserver(()=>{
moveIfPresent(SEL.infoBlock, '#tab-info');
moveIfPresent(SEL.comments, '#tab-comments');
moveIfPresent(SEL.related, '#tab-videos');
if (state.settings.features.transcriptTab) {
const tp = $first(SEL.transcriptPanel);
if (tp) document.querySelector('#tab-transcript')?.append(tp);
}
});
mo.observe(document, { subtree:true, childList:true });
// Auto-expand description (where supported)
if (state.settings.features.autoExpandDesc) {
const tryExpand = () => {
const more = document.querySelector('#expand, tp-yt-paper-button[aria-label*="more"], #description tp-yt-paper-button[aria-label*="more"]');
if (more) more.click();
};
tryExpand();
setTimeout(tryExpand, 1000);
}
Speed.loadPreferred();
Speed.mount();
Ads.activate();
Transcript.ensureTabVisibility();
// Identify video & channel ids (best-effort)
try {
const app = $first(SEL.ytcApp);
const data = app?.__data?.data?.response || app?.data?.response || null;
state.videoId = document.querySelector('ytd-watch-flexy')?.__data?.playerResponse?.videoDetails?.videoId
|| new URL(location.href).searchParams.get('v') || null;
state.channelId = data?.contents?.twoColumnWatchNextResults?.results?.results?.contents
?.find(c=>c.videoSecondaryInfoRenderer)?.videoSecondaryInfoRenderer?.owner?.videoOwnerRenderer?.navigationEndpoint?.browseEndpoint?.browseId || null;
} catch {}
Tabs.restoreLastTab();
}
return { run };
})();
/********************************************************************
* Theme (system / light / dark)
********************************************************************/
const Theme = (() => {
function apply() {
const mode = state.settings.ui.theme;
// Keep it simple: let YouTube manage its theme; only nudge if set explicitly.
document.documentElement.removeAttribute('de-theme');
if (mode==='light') document.documentElement.setAttribute('de-theme', 'light');
else if (mode==='dark') document.documentElement.setAttribute('de-theme', 'dark');
// (If you want a cookie-based PREF hack, we can add it, but it’s invasive.)
}
return { apply };
})();
/********************************************************************
* Bootstrap
********************************************************************/
function init() {
// Apply theme CSS var
document.documentElement.style.setProperty('--de-rounded', `${state.settings.ui.rounded}px`);
Theme.apply();
Shortcuts.init();
onReady(() => {
WireUp.run();
});
}
init();
})();