Greasy Fork is available in English.
Forces YouTube Subscriptions feed into a classic List View with optional video descriptions. Press Alt+L to open settings.
// ==UserScript==
// @name YouTube List View
// @namespace http://tampermonkey.net/
// @version 5.2
// @description Forces YouTube Subscriptions feed into a classic List View with optional video descriptions. Press Alt+L to open settings.
// @author dr.bobo0
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?domain=youtube.com
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_addStyle
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ══════════════════════════════════════════════════════════════════
// CONSTANTS & CONFIG
// ══════════════════════════════════════════════════════════════════
const SHIFT_DELAY = 600;
const GRID_RECTS = [
{ x: 2, y: 4, width: 5, height: 7, rx: 1 },
{ x: 2, y: 13, width: 5, height: 7, rx: 1 },
{ x: 9.5, y: 4, width: 5, height: 7, rx: 1 },
{ x: 9.5, y: 13, width: 5, height: 7, rx: 1 },
{ x: 17, y: 4, width: 5, height: 7, rx: 1 },
{ x: 17, y: 13, width: 5, height: 7, rx: 1 },
];
const LIST_RECTS = [
{ x: 2, y: 4, width: 5, height: 4, rx: 1 },
{ x: 9, y: 4, width: 13, height: 4, rx: 1 },
{ x: 2, y: 10, width: 5, height: 4, rx: 1 },
{ x: 9, y: 10, width: 13, height: 4, rx: 1 },
{ x: 2, y: 16, width: 5, height: 4, rx: 1 },
{ x: 9, y: 16, width: 13, height: 4, rx: 1 },
];
const SLIDER_CONFIGS = [
{ key: 'listContainerWidth', label: 'Container Width', min: 51, max: 100, unit: '%', cssVar: '--list-container-width', icon: '⇔' },
{ key: 'thumbnailWidth', label: 'Thumbnail Width', min: 150, max: 400, unit: 'px', cssVar: '--thumbnail-width', icon: '⊡' },
{ key: 'titleFontSize', label: 'Title Size', min: 10, max: 40, unit: 'pt', cssVar: '--title-font-size', icon: 'T' },
{ key: 'metaFontSize', label: 'Meta Size', min: 8, max: 24, unit: 'pt', cssVar: '--meta-font-size', icon: 't' },
{ key: 'notifyWidth', label: 'Notify Button', min: 100, max: 700, unit: 'px', cssVar: '--notify-width', icon: '🔔' },
];
// Toggles rendered in the panel
const TOGGLE_CONFIGS = [
{ key: 'changeShortsScroll', label: 'Shorts: scroll back on collapse', icon: '↑' },
{ key: 'hideShorts', label: 'Hide Shorts section', icon: '🚫' },
{ key: 'hideMostRelevant', label: 'Hide "Most Relevant" section', icon: '🚫' },
{ key: 'hideDividers', label: 'Hide row dividers', icon: '―' },
{ key: 'showDescriptions', label: 'Show video descriptions', icon: '≡', warn: 'Fetches each video page — uses extra bandwidth. Cached for 1 hour.' },
{ key: 'panelTransparent', label: 'Frosted glass panel', icon: '◫' },
];
const DEFAULT_SETTINGS = {
listContainerWidth: 90,
thumbnailWidth: 260,
titleFontSize: 13,
metaFontSize: 10,
notifyWidth: 150,
viewModeSubs: 'list',
changeShortsScroll: false,
hideShorts: false,
hideMostRelevant: false,
hideDividers: false,
showDescriptions: false,
panelTransparent: false,
panelLight: false,
};
// Description cache config
const DESC_CACHE_KEY = 'ytlv_desc_cache_v1';
const DESC_CACHE_TTL_MS = 60 * 60 * 1000;
const DESC_MAX_ENTRIES = 800;
const DESC_SAVE_DELAY = 300;
// Shimmer
const SHIMMER_VAR = '--ytlv-shimmer-x';
const SHIMMER_MS = 2000;
// ══════════════════════════════════════════════════════════════════
// DOM HELPERS
// ══════════════════════════════════════════════════════════════════
function createSVG(rects) {
const NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
for (const attrs of rects) {
const rect = document.createElementNS(NS, 'rect');
for (const [k, v] of Object.entries(attrs)) rect.setAttribute(k, v);
svg.appendChild(rect);
}
return svg;
}
function makeSVGIcon(pathD, size = 20) {
const NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', String(size));
svg.setAttribute('height', String(size));
const path = document.createElementNS(NS, 'path');
path.setAttribute('d', pathD);
svg.appendChild(path);
return svg;
}
function el(tag, attrs = {}, children = []) {
const e = document.createElement(tag);
for (const [k, v] of Object.entries(attrs)) {
if (k === 'style' && typeof v === 'object') Object.assign(e.style, v);
else if (k === 'className') e.className = v;
else if (k === 'textContent') e.textContent = v;
else if (k === 'htmlFor') e.htmlFor = v;
else e.setAttribute(k, v);
}
for (const child of children) {
if (typeof child === 'string') e.appendChild(document.createTextNode(child));
else if (child) e.appendChild(child);
}
return e;
}
// ══════════════════════════════════════════════════════════════════
// PAGE HELPERS (subscriptions only)
// ══════════════════════════════════════════════════════════════════
function isSubs() { return location.pathname === '/feed/subscriptions'; }
function isTargetPage() { return isSubs(); }
// ══════════════════════════════════════════════════════════════════
// SETTINGS STORAGE
// ══════════════════════════════════════════════════════════════════
function getSetting(key) {
const val = GM_getValue(key, DEFAULT_SETTINGS[key]);
if (typeof DEFAULT_SETTINGS[key] === 'boolean') return val === true || val === 'true';
if (typeof DEFAULT_SETTINGS[key] === 'number') return Number(val);
return val;
}
function setSetting(key, value) { GM_setValue(key, value); }
function getAllSettings() {
const s = {};
for (const key of Object.keys(DEFAULT_SETTINGS)) s[key] = getSetting(key);
return s;
}
let settings = getAllSettings();
// ══════════════════════════════════════════════════════════════════
// DESCRIPTION CACHE
// ══════════════════════════════════════════════════════════════════
const _descState = {
memCache: new Map(),
persist: null,
dirty: false,
saveTimer: 0,
inFlight: new Map(), // vid → true (dedup only)
loopTimer: 0,
shimRaf: 0,
shimRunning: false,
shimT0: 0,
};
function descStoreLoad() {
if (_descState.persist) return;
let obj = {};
try {
const raw = localStorage.getItem(DESC_CACHE_KEY) || '';
if (raw) {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') obj = parsed;
}
} catch { obj = {}; }
_descState.persist = obj;
descStorePrune();
}
function descStoreSave() {
if (_descState.saveTimer) return;
_descState.saveTimer = setTimeout(() => {
_descState.saveTimer = 0;
if (!_descState.dirty) return;
_descState.dirty = false;
try { localStorage.setItem(DESC_CACHE_KEY, JSON.stringify(_descState.persist || {})); } catch { }
}, DESC_SAVE_DELAY);
}
function descStorePrune() {
descStoreLoad();
const now = Date.now();
const obj = _descState.persist || {};
const entries = [];
for (const k of Object.keys(obj)) {
const t = Number(obj[k]?.t || 0);
if (!t || now - t >= DESC_CACHE_TTL_MS) { delete obj[k]; _descState.dirty = true; continue; }
entries.push([k, t]);
}
if (entries.length > DESC_MAX_ENTRIES) {
entries.sort((a, b) => a[1] - b[1]);
for (let i = 0; i < entries.length - DESC_MAX_ENTRIES; i++) {
delete obj[entries[i][0]]; _descState.dirty = true;
}
}
if (_descState.dirty) descStoreSave();
}
function descGet(vid) {
if (!vid) return null;
descStoreLoad();
if (_descState.memCache.has(vid)) return _descState.memCache.get(vid);
const e = _descState.persist[vid];
if (!e) return null;
if (Date.now() - Number(e.t || 0) >= DESC_CACHE_TTL_MS) {
delete _descState.persist[vid]; _descState.dirty = true; descStoreSave(); return null;
}
_descState.memCache.set(vid, e.d);
return e.d;
}
function descSet(vid, text) {
if (!vid) return;
descStoreLoad();
_descState.memCache.set(vid, text);
_descState.persist[vid] = { t: Date.now(), d: String(text || '') };
_descState.dirty = true;
descStorePrune();
descStoreSave();
}
// ══════════════════════════════════════════════════════════════════
// DESCRIPTION FETCHING
// ══════════════════════════════════════════════════════════════════
function decodeHtmlEntities(str) {
return (str || '').replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'");
}
function extractVideoId(href) {
const h = String(href || '');
if (h.includes('/shorts/')) {
const m = h.match(/\/shorts\/([^?&#/]+)/);
return m ? m[1] : '';
}
try { return new URL(h, location.origin).searchParams.get('v') || ''; } catch { return ''; }
}
function applyDescToDom(vid, text) {
document.querySelectorAll(`.custom-description[data-vid="${CSS.escape(vid)}"]`).forEach(el => {
el.classList.remove('desc-skeleton');
el.textContent = text || '';
el.style.display = text ? '' : 'none';
});
}
// V4-style: just grab the <meta> tag — no JSON parsing, no post-processing
function fetchDescription(item) {
if (!settings.showDescriptions || !listViewEnabled) return;
const linkEl = item.querySelector('a#video-title-link, a.yt-lockup-metadata-view-model__title');
if (!linkEl) return;
const metaContainer = item.querySelector('yt-content-metadata-view-model');
if (!metaContainer) return;
const vid = extractVideoId(linkEl.href);
if (!vid) return;
// Already resolved for this vid
const descEl = metaContainer.querySelector('.custom-description');
if (descEl?.dataset.vid === vid && !descEl.classList.contains('desc-skeleton')) return;
// Serve from cache instantly — no fetch needed
const cached = descGet(vid);
if (cached != null) {
applyDescToDom(vid, cached);
return;
}
// Already in flight
if (_descState.inFlight.has(vid)) return;
_descState.inFlight.set(vid, true);
fetch(linkEl.href)
.then(r => r.text())
.then(html => {
const m = html.match(/<meta name="description" content="([^"]*)"/);
const text = (m && m[1] && m[1] !== 'null') ? decodeHtmlEntities(m[1]) : '';
descSet(vid, text);
applyDescToDom(vid, text);
})
.catch(() => { descSet(vid, ''); applyDescToDom(vid, ''); })
.finally(() => { _descState.inFlight.delete(vid); });
}
function ensureDescLoop() {
if (_descState.loopTimer) clearInterval(_descState.loopTimer);
_descState.loopTimer = setInterval(() => {
if (!settings.showDescriptions) { stopShimmer(); return; }
descStorePrune();
if (document.querySelector('.custom-description.desc-skeleton')) startShimmer();
else stopShimmer();
}, 400);
}
// ══════════════════════════════════════════════════════════════════
// SHIMMER ANIMATION
// ══════════════════════════════════════════════════════════════════
function startShimmer() {
if (_descState.shimRunning) return;
_descState.shimRunning = true;
_descState.shimT0 = performance.now();
const tick = t => {
if (!_descState.shimRunning) return;
if (!document.querySelector('.custom-description.desc-skeleton')) { stopShimmer(); return; }
const phase = (t - _descState.shimT0) % SHIMMER_MS / SHIMMER_MS;
document.documentElement.style.setProperty(SHIMMER_VAR, `${200 - phase * 400}%`);
_descState.shimRaf = requestAnimationFrame(tick);
};
_descState.shimRaf = requestAnimationFrame(tick);
}
function stopShimmer() {
_descState.shimRunning = false;
if (_descState.shimRaf) { cancelAnimationFrame(_descState.shimRaf); _descState.shimRaf = 0; }
document.documentElement.style.removeProperty(SHIMMER_VAR);
}
// ══════════════════════════════════════════════════════════════════
// CSS
// ══════════════════════════════════════════════════════════════════
const FEED = 'ytd-browse[page-subtype="subscriptions"]';
const FEED_ITEM = `html.list-view-active ${FEED} ytd-rich-grid-renderer > #contents > ytd-rich-item-renderer`;
GM_addStyle(`
:root {
--list-container-width: 90%;
--thumbnail-width: 260px;
--header-height: 40px;
--channel-info-padding-bottom: 10px;
--notify-width: 150px;
--title-font-size: 13pt;
--meta-font-size: 10pt;
/* Panel design tokens — dark (default) */
--ytlv-bg: #0f0f0f;
--ytlv-bg-elevated: #212121;
--ytlv-bg-hover: #3f3f3f;
--ytlv-surface: #1f1f1f;
--ytlv-border: rgba(255,255,255,0.1);
--ytlv-text-primary: #f1f1f1;
--ytlv-text-secondary:#aaaaaa;
--ytlv-text-muted: #717171;
--ytlv-accent: #ff0000;
--ytlv-blue: #3ea6ff;
--ytlv-radius-sm: 4px;
--ytlv-radius-md: 8px;
--ytlv-radius-lg: 12px;
--ytlv-radius-full: 100px;
--ytlv-shadow: 0 4px 20px rgba(0,0,0,0.5), 0 1px 4px rgba(0,0,0,0.3);
--ytlv-chip-hover-bg: rgba(255,255,255,0.1);
--ytlv-chip-active-bg: rgba(255,255,255,0.15);
--ytlv-chip-active-border: rgba(255,255,255,0.35);
--ytlv-switch-off: rgba(255,255,255,0.2);
--ytlv-input-bg: rgba(255,255,255,0.06);
--ytlv-input-border: rgba(255,255,255,0.12);
--ytlv-input-border-focus: rgba(255,255,255,0.35);
--ytlv-input-bg-focus: rgba(255,255,255,0.1);
--ytlv-toggle-row-hover: rgba(255,255,255,0.04);
--ytlv-scrollbar-thumb: rgba(255,255,255,0.12);
--ytlv-shortcut-bg: rgba(255,255,255,0.06);
--ytlv-shortcut-border: rgba(255,255,255,0.1);
--ytlv-reset-border: rgba(255,255,255,0.12);
--ytlv-reset-hover-bg: rgba(255,255,255,0.08);
--ytlv-reset-hover-border: rgba(255,255,255,0.3);
--ytlv-reset-active-bg: rgba(255,255,255,0.14);
--ytlv-slider-track-bg: rgba(255,255,255,0.15);
--ytlv-thumb-border: #0f0f0f;
--ytlv-thumb-shadow: rgba(255,255,255,0.3);
}
/* ── Light theme overrides for panel ── */
#yt-lv-panel.panel-light {
--ytlv-bg: #ffffff;
--ytlv-bg-elevated: #f2f2f2;
--ytlv-surface: #f9f9f9;
--ytlv-border: rgba(0,0,0,0.1);
--ytlv-text-primary: #0f0f0f;
--ytlv-text-secondary:#606060;
--ytlv-text-muted: #909090;
--ytlv-blue: #065fd4;
--ytlv-shadow: 0 4px 20px rgba(0,0,0,0.15), 0 1px 4px rgba(0,0,0,0.08);
--ytlv-chip-hover-bg: rgba(0,0,0,0.07);
--ytlv-chip-active-bg: rgba(0,0,0,0.1);
--ytlv-chip-active-border: rgba(0,0,0,0.3);
--ytlv-switch-off: rgba(0,0,0,0.18);
--ytlv-input-bg: rgba(0,0,0,0.04);
--ytlv-input-border: rgba(0,0,0,0.12);
--ytlv-input-border-focus: rgba(0,0,0,0.3);
--ytlv-input-bg-focus: rgba(0,0,0,0.07);
--ytlv-toggle-row-hover: rgba(0,0,0,0.04);
--ytlv-scrollbar-thumb: rgba(0,0,0,0.15);
--ytlv-shortcut-bg: rgba(0,0,0,0.05);
--ytlv-shortcut-border: rgba(0,0,0,0.12);
--ytlv-reset-border: rgba(0,0,0,0.15);
--ytlv-reset-hover-bg: rgba(0,0,0,0.06);
--ytlv-reset-hover-border: rgba(0,0,0,0.3);
--ytlv-reset-active-bg: rgba(0,0,0,0.1);
--ytlv-slider-track-bg: rgba(0,0,0,0.15);
--ytlv-thumb-border: #ffffff;
--ytlv-thumb-shadow: rgba(0,0,0,0.25);
scrollbar-color: rgba(0,0,0,0.15) transparent;
}
#yt-lv-panel.panel-light::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.15); }
/* ── Hide Shorts ── */
html.hide-shorts ${FEED} ytd-rich-section-renderer:has(ytd-rich-shelf-renderer[is-shorts]),
html.hide-shorts ${FEED} ytd-rich-shelf-renderer[is-shorts] {
display: none !important;
}
/* ── Toggle Buttons — YouTube glass-chip style ── */
#view-toggle-container {
display: flex; align-items: center; gap: 6px; padding-left: 8px; z-index: 2018;
}
.view-toggle-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 0 12px 0 10px;
height: 32px;
background: rgba(255,255,255,0.1);
border: none;
outline: none;
border-radius: 100px;
cursor: pointer;
color: var(--yt-spec-text-primary, #f1f1f1);
font-family: "Roboto", "Arial", sans-serif;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
transition: background 0.15s, color 0.15s;
}
.view-toggle-btn:focus-visible {
outline: 2px solid var(--yt-spec-call-to-action, #3ea6ff);
outline-offset: 2px;
}
.view-toggle-btn:hover { background: rgba(255,255,255,0.18); }
.view-toggle-btn.active {
background: var(--yt-spec-text-primary, #f1f1f1);
color: var(--yt-spec-base-background, #0f0f0f);
}
.view-toggle-btn svg { width: 16px; height: 16px; fill: currentColor; flex-shrink: 0; }
.view-toggle-btn .vt-label { font-size: 13px; font-weight: 500; line-height: 1; }
html:not([dark]) .view-toggle-btn { background: rgba(0,0,0,0.05); color: var(--yt-spec-text-primary, #0f0f0f); }
html:not([dark]) .view-toggle-btn:hover { background: rgba(0,0,0,0.1); }
html:not([dark]) .view-toggle-btn.active { background: var(--yt-spec-text-primary, #0f0f0f); color: #fff; }
/* ── Grid Override ── */
html.list-view-active ${FEED} ytd-rich-grid-renderer {
--ytd-rich-grid-items-per-row: 1 !important;
--ytd-rich-grid-posts-per-row: 1 !important;
width: 100% !important; max-width: 100% !important; margin: 0 !important;
}
html.list-view-active ${FEED} #contents {
width: 100% !important; max-width: 100% !important;
}
/* ── Item Container ── */
${FEED_ITEM} {
width: 100% !important; max-width: var(--list-container-width) !important;
padding-bottom: 16px !important; margin: 0 auto 16px auto !important;
border-bottom: 1px solid #3F3F3F !important;
}
${FEED_ITEM}:last-child { border-bottom: none !important; }
/* ── Lockup Row Layout ── */
${FEED_ITEM} .yt-lockup-view-model {
display: flex !important; flex-direction: row !important;
align-items: flex-start !important; gap: 16px !important;
width: 100% !important; justify-content: flex-start !important;
position: relative !important;
padding-top: calc(var(--header-height) + var(--channel-info-padding-bottom)) !important;
isolation: isolate;
}
/* ── Thumbnail ── */
html.list-view-active ${FEED} .yt-lockup-view-model__content-image {
width: var(--thumbnail-width) !important; min-width: var(--thumbnail-width) !important;
max-width: var(--thumbnail-width) !important; flex-shrink: 0 !important;
display: block !important; margin: 0 !important;
border-radius: 12px !important; overflow: hidden !important;
position: relative !important; z-index: auto !important;
}
/* ── Metadata Reset ── */
html.list-view-active ${FEED} :is(
.yt-lockup-view-model__metadata,
yt-lockup-metadata-view-model,
.yt-lockup-metadata-view-model,
.yt-lockup-metadata-view-model__text-container,
yt-content-metadata-view-model
) { position: static !important; transform: none !important; contain: none !important; }
/* ── Title ── */
html.list-view-active ${FEED} .yt-lockup-metadata-view-model__title {
font-size: var(--title-font-size) !important; line-height: 1.3 !important;
margin-bottom: 6px !important; max-width: 90% !important; display: block !important;
}
/* ── Avatar ── */
html.list-view-active ${FEED} .yt-lockup-metadata-view-model__avatar {
position: absolute !important; top: 0 !important; left: 0 !important;
width: 32px !important; height: 32px !important;
z-index: auto !important; margin: 0 !important; border-radius: 50% !important;
}
html.list-view-active ${FEED} .custom-avatar-link {
display: block !important; width: 100% !important; height: 100% !important;
border-radius: 50% !important; cursor: pointer !important;
}
html.list-view-active ${FEED} .yt-lockup-metadata-view-model__avatar img {
width: 100% !important; height: 100% !important;
display: block !important; border-radius: 50% !important;
}
/* ── Cloned Channel Name ── */
${FEED} .cloned-channel-name { display: none !important; }
html.list-view-active ${FEED} .cloned-channel-name {
position: absolute !important; top: 0 !important; left: 42px !important;
height: 32px !important; z-index: auto !important; background: transparent !important;
padding: 0 !important; display: flex !important; align-items: center !important;
pointer-events: auto !important;
}
html.list-view-active ${FEED} .cloned-channel-name :is(a, span) {
color: var(--yt-spec-text-primary) !important; font-weight: 500 !important;
font-size: var(--title-font-size) !important; text-shadow: none !important;
text-decoration: none !important; line-height: 1 !important;
}
html.list-view-active ${FEED} .cloned-channel-name span[role="separator"] {
display: none !important;
}
/* ── Content Metadata Rows ── */
html.list-view-active ${FEED} yt-content-metadata-view-model {
display: block !important; width: 100% !important;
}
html.list-view-active ${FEED} yt-content-metadata-view-model
.yt-content-metadata-view-model__metadata-row:not(.cloned-channel-name) {
display: inline-flex !important; align-items: baseline !important;
margin: 0 !important; padding: 0 !important;
font-size: var(--meta-font-size) !important; color: #aaa !important;
white-space: nowrap !important;
}
html.list-view-active ${FEED} yt-content-metadata-view-model
.yt-content-metadata-view-model__metadata-row:not(.cloned-channel-name) * {
font-size: var(--meta-font-size) !important; line-height: 1.4 !important;
}
html.list-view-active ${FEED} yt-content-metadata-view-model
.yt-content-metadata-view-model__metadata-row:not(.cloned-channel-name):first-of-type {
margin-right: 8px !important;
}
html.list-view-active ${FEED} yt-content-metadata-view-model
.yt-content-metadata-view-model__metadata-row:not(.cloned-channel-name) a {
color: #aaa !important; font-weight: 400 !important; text-decoration: none !important;
}
html.list-view-active ${FEED} yt-content-metadata-view-model
.yt-content-metadata-view-model__metadata-row:not(.cloned-channel-name) a:hover {
color: #fff !important;
}
html.list-view-active ${FEED} yt-content-metadata-view-model
.yt-content-metadata-view-model__metadata-row:not(.cloned-channel-name) span[role="separator"] {
display: inline-block !important; margin: 0 4px !important;
}
/* ── Menu Button ── */
html.list-view-active ${FEED} .yt-lockup-metadata-view-model__menu-button {
position: absolute !important;
top: calc(var(--header-height) + var(--channel-info-padding-bottom)) !important;
right: 0 !important;
}
/* ── Sections & Shelves ── */
html.list-view-active ${FEED} ytd-rich-section-renderer > #content {
max-width: var(--list-container-width) !important; margin: 0 auto !important;
}
html.list-view-active ${FEED} ytd-rich-section-renderer,
html.list-view-active ${FEED} ytd-shelf-renderer,
html.list-view-active ${FEED} ytd-shelf-renderer h2 {
margin: 0 !important; padding: 0 !important;
}
/* ── Notify Button ── */
html.list-view-active :is(lockup-attachments-view-model, .ytLockupAttachmentsViewModelHost) {
width: var(--notify-width) !important; max-width: 100% !important;
}
/* ── Dismissed / Replaced Items ── */
${FEED_ITEM}:has(.ytDismissibleItemReplacedContent) .yt-lockup-view-model {
display: block !important; padding-top: 0 !important; height: auto !important;
}
html.list-view-active ytd-rich-item-renderer:has(.ytDismissibleItemReplacedContent)
:is(.ytDismissibleItemAspectRatio16By9, .ytDismissibleItemReplacedContent) {
padding: 0 !important; aspect-ratio: auto !important;
height: auto !important; min-height: 0 !important;
}
html.list-view-active ytd-rich-item-renderer:has(.ytDismissibleItemReplacedContent)
.ytDismissibleItemAspectRatio16By9::before {
display: none !important; padding-bottom: 0 !important;
}
html.list-view-active ytd-rich-item-renderer:has(.ytDismissibleItemReplacedContent)
.ytDismissibleItemAspectRatioContainer {
position: static !important; padding: 24px !important;
display: flex !important; justify-content: center !important;
align-items: center !important; height: auto !important;
}
html.list-view-active ytd-rich-item-renderer:has(.ytDismissibleItemReplacedContent)
notification-multi-action-renderer {
position: static !important; height: auto !important;
width: auto !important; margin: 0 auto !important;
}
/* ── Hide Dividers ── */
html.list-view-active.hide-dividers ${FEED}
ytd-rich-grid-renderer > #contents > ytd-rich-item-renderer {
border-bottom: none !important;
}
/* ── Video Descriptions ── */
.custom-description {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
font-size: var(--meta-font-size);
line-height: 1.5;
color: #9a9a9a;
margin-top: 8px;
width: 100%;
max-width: 95%;
white-space: normal;
}
html:not(.show-descriptions) .custom-description { display: none !important; }
html:not(.list-view-active) .custom-description { display: none !important; }
/* Shimmer skeleton state */
.custom-description.desc-skeleton {
display: block !important;
-webkit-line-clamp: unset !important;
overflow: hidden !important;
color: transparent !important;
pointer-events: none !important;
}
.custom-description.desc-skeleton::before,
.custom-description.desc-skeleton::after {
content: '';
display: block;
border-radius: 6px;
height: 11px;
background-color: var(--ytlv-skel-base, rgba(255,255,255,.10));
background-image: linear-gradient(
90deg,
var(--ytlv-skel-base, rgba(255,255,255,.10)) 0%,
var(--yt-spec-10-percent-layer, rgba(255,255,255,.22)) 50%,
var(--ytlv-skel-base, rgba(255,255,255,.10)) 100%
);
background-size: 220% 100%;
background-position: var(${SHIMMER_VAR}, 200%) 0;
will-change: background-position;
}
.custom-description.desc-skeleton::before { width: 78%; margin-bottom: 6px; }
.custom-description.desc-skeleton::after { width: 55%; }
html[dark] { --ytlv-skel-base: rgba(255,255,255,.10); }
html:not([dark]) { --ytlv-skel-base: rgba(0,0,0,.10); }
/* ══════════════════════════════════════════════════════════
SETTINGS PANEL
══════════════════════════════════════════════════════════ */
#yt-lv-panel-overlay {
position: fixed; inset: 0; z-index: 2147483645;
background: transparent; display: none; pointer-events: none;
}
#yt-lv-panel-overlay.open { display: block; pointer-events: auto; }
/* Panel shell */
#yt-lv-panel {
position: fixed;
top: 56px;
right: 16px;
width: 360px;
max-height: calc(100vh - 72px);
overflow-y: auto;
overflow-x: hidden;
font-family: "Roboto", "Arial", sans-serif;
font-size: 14px;
background: var(--ytlv-bg, #0f0f0f);
color: var(--ytlv-text-primary, #f1f1f1);
border-radius: var(--ytlv-radius-lg, 12px);
border: 1px solid var(--ytlv-border, rgba(255,255,255,0.1));
box-shadow: var(--ytlv-shadow);
z-index: 2147483647;
box-sizing: border-box;
display: none;
scrollbar-width: thin;
scrollbar-color: var(--ytlv-scrollbar-thumb, rgba(255,255,255,0.12)) transparent;
transform-origin: top right;
transition: background 0.2s, color 0.2s, border-color 0.2s, box-shadow 0.2s;
}
#yt-lv-panel::-webkit-scrollbar { width: 6px; }
#yt-lv-panel::-webkit-scrollbar-thumb { background: var(--ytlv-scrollbar-thumb, rgba(255,255,255,0.12)); border-radius: 3px; }
#yt-lv-panel::-webkit-scrollbar-track { background: transparent; }
#yt-lv-panel.open {
display: block;
animation: ytlv-panel-in 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
}
#yt-lv-panel.panel-transparent {
background: rgba(15,15,15,0.88);
backdrop-filter: blur(20px) saturate(1.4);
-webkit-backdrop-filter: blur(20px) saturate(1.4);
}
#yt-lv-panel.panel-light.panel-transparent {
background: rgba(255,255,255,0.88);
}
@keyframes ytlv-panel-in {
from { opacity: 0; transform: scale(0.96) translateY(-6px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
/* Header */
#yt-lv-panel-header {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px 12px;
border-bottom: 1px solid var(--ytlv-border, rgba(255,255,255,0.1));
cursor: move;
user-select: none;
position: sticky;
top: 0;
background: var(--ytlv-bg, #0f0f0f);
z-index: 2;
border-radius: var(--ytlv-radius-lg) var(--ytlv-radius-lg) 0 0;
transition: background 0.2s, border-color 0.2s;
}
#yt-lv-panel.panel-transparent #yt-lv-panel-header {
background: rgba(15,15,15,0.82);
backdrop-filter: blur(20px) saturate(1.4);
-webkit-backdrop-filter: blur(20px) saturate(1.4);
}
#yt-lv-panel.panel-light.panel-transparent #yt-lv-panel-header {
background: rgba(255,255,255,0.82);
backdrop-filter: blur(20px) saturate(1.4);
-webkit-backdrop-filter: blur(20px) saturate(1.4);
}
#yt-lv-panel-header-icon {
width: 20px; height: 20px; flex-shrink: 0;
fill: var(--ytlv-text-primary, #f1f1f1);
}
#yt-lv-panel-title {
flex: 1;
font-size: 14px;
font-weight: 500;
color: var(--ytlv-text-primary, #f1f1f1);
letter-spacing: 0.01em;
}
#yt-lv-panel-shortcut {
font-size: 11px;
color: var(--ytlv-text-muted, #717171);
background: var(--ytlv-shortcut-bg, rgba(255,255,255,0.06));
border: 1px solid var(--ytlv-shortcut-border, rgba(255,255,255,0.1));
border-radius: var(--ytlv-radius-sm, 4px);
padding: 2px 6px;
font-family: monospace;
letter-spacing: 0.05em;
}
/* Theme toggle button */
#yt-lv-theme-btn {
width: 30px; height: 30px;
display: flex; align-items: center; justify-content: center;
background: none; border: none; cursor: pointer;
color: var(--ytlv-text-muted, #717171);
border-radius: 50%;
flex-shrink: 0;
transition: background 0.15s, color 0.15s;
padding: 0; position: relative;
}
#yt-lv-theme-btn:hover {
background: var(--ytlv-chip-hover-bg, rgba(255,255,255,0.1));
color: var(--ytlv-text-primary, #f1f1f1);
}
#yt-lv-theme-btn svg {
width: 18px; height: 18px; fill: currentColor; pointer-events: none;
transition: transform 0.3s cubic-bezier(0.4,0,0.2,1), opacity 0.2s;
position: absolute;
}
/* Dark (default): sun visible */
#yt-lv-panel:not(.panel-light) #yt-lv-theme-btn .icon-sun { opacity: 1; transform: rotate(0deg) scale(1); }
#yt-lv-panel:not(.panel-light) #yt-lv-theme-btn .icon-moon { opacity: 0; transform: rotate(90deg) scale(0.5); }
/* Light: moon visible */
#yt-lv-panel.panel-light #yt-lv-theme-btn .icon-sun { opacity: 0; transform: rotate(-90deg) scale(0.5); }
#yt-lv-panel.panel-light #yt-lv-theme-btn .icon-moon { opacity: 1; transform: rotate(0deg) scale(1); }
#yt-lv-panel-close {
width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
background: none; border: none; cursor: pointer;
color: var(--ytlv-text-muted, #717171);
border-radius: 50%;
transition: background 0.15s, color 0.15s;
flex-shrink: 0;
}
#yt-lv-panel-close:hover {
background: var(--ytlv-chip-hover-bg, rgba(255,255,255,0.1));
color: var(--ytlv-text-primary, #f1f1f1);
}
#yt-lv-panel-close svg { width: 18px; height: 18px; fill: currentColor; pointer-events: none; }
/* Panel body */
#yt-lv-panel-body { padding: 12px 0 8px; }
.ytlv-section-label {
font-size: 11px;
font-weight: 500;
color: var(--ytlv-text-muted, #717171);
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 0 16px;
margin: 4px 0 8px;
}
.ytlv-divider {
height: 1px;
background: var(--ytlv-border, rgba(255,255,255,0.1));
margin: 10px 0;
}
/* View mode chips */
#ytlv-view-chips { display: flex; gap: 8px; padding: 0 16px; margin-bottom: 4px; }
.ytlv-chip {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px 6px 10px;
border-radius: var(--ytlv-radius-full, 100px);
border: 1px solid var(--ytlv-border, rgba(255,255,255,0.15));
background: transparent;
color: var(--ytlv-text-secondary, #aaa);
font-size: 13px;
font-weight: 400;
cursor: pointer;
transition: all 0.15s;
user-select: none;
}
.ytlv-chip:hover {
background: var(--ytlv-chip-hover-bg, rgba(255,255,255,0.1));
color: var(--ytlv-text-primary, #f1f1f1);
border-color: var(--ytlv-chip-active-border, rgba(255,255,255,0.25));
}
.ytlv-chip.active {
background: var(--ytlv-chip-active-bg, rgba(255,255,255,0.15));
color: var(--ytlv-text-primary, #f1f1f1);
border-color: var(--ytlv-chip-active-border, rgba(255,255,255,0.35));
font-weight: 500;
}
.ytlv-chip svg { width: 16px; height: 16px; fill: currentColor; flex-shrink: 0; }
/* Slider rows */
.ytlv-slider-row { padding: 6px 16px; display: flex; flex-direction: column; gap: 5px; }
.ytlv-slider-label { font-size: 13px; color: var(--ytlv-text-secondary, #aaa); }
.ytlv-slider-track { display: flex; align-items: center; gap: 8px; }
.ytlv-slider-track input[type="range"] {
flex: 1; height: 3px;
appearance: none; -webkit-appearance: none;
background: var(--ytlv-slider-track-bg, rgba(255,255,255,0.15));
border-radius: 2px; outline: none; cursor: pointer;
accent-color: var(--ytlv-text-primary, #f1f1f1);
}
.ytlv-slider-track input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px; height: 14px; border-radius: 50%;
background: var(--ytlv-text-primary, #f1f1f1);
border: 2px solid var(--ytlv-thumb-border, #0f0f0f);
box-shadow: 0 0 0 1px var(--ytlv-thumb-shadow, rgba(255,255,255,0.3));
transition: transform 0.1s;
}
.ytlv-slider-track input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.2); }
.ytlv-slider-track input[type="range"]::-moz-range-thumb {
width: 14px; height: 14px; border-radius: 50%;
background: var(--ytlv-text-primary, #f1f1f1);
border: 2px solid var(--ytlv-thumb-border, #0f0f0f);
}
.ytlv-num-input {
width: 54px;
background: var(--ytlv-input-bg, rgba(255,255,255,0.06));
border: 1px solid var(--ytlv-input-border, rgba(255,255,255,0.12));
border-radius: var(--ytlv-radius-sm, 4px);
color: var(--ytlv-text-primary, #f1f1f1);
font-size: 12px; padding: 4px 6px; text-align: right;
font-variant-numeric: tabular-nums;
transition: border-color 0.15s, background 0.15s;
}
.ytlv-num-input:focus {
outline: none;
border-color: var(--ytlv-input-border-focus, rgba(255,255,255,0.35));
background: var(--ytlv-input-bg-focus, rgba(255,255,255,0.1));
}
/* Toggle rows */
.ytlv-toggle-row {
display: flex; align-items: center; justify-content: space-between;
padding: 9px 16px;
transition: background 0.12s; cursor: pointer; gap: 12px;
}
.ytlv-toggle-row:hover { background: var(--ytlv-toggle-row-hover, rgba(255,255,255,0.04)); }
.ytlv-toggle-label { font-size: 13px; color: var(--ytlv-text-secondary, #aaa); flex: 1; cursor: pointer; }
.ytlv-toggle-warn { font-size: 11px; color: var(--ytlv-text-muted, #717171); padding: 0 16px 6px; line-height: 1.4; }
/* YouTube-style toggle switch */
.ytlv-switch { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
.ytlv-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
.ytlv-switch-track {
position: absolute; inset: 0;
background: var(--ytlv-switch-off, rgba(255,255,255,0.2));
border-radius: 10px; transition: background 0.2s; cursor: pointer;
}
.ytlv-switch input:checked + .ytlv-switch-track { background: var(--ytlv-blue, #3ea6ff); }
.ytlv-switch-thumb {
position: absolute; top: 2px; left: 2px;
width: 16px; height: 16px; border-radius: 50%; background: #fff;
transition: transform 0.2s cubic-bezier(0.4,0,0.2,1), box-shadow 0.15s;
pointer-events: none; box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.ytlv-switch input:checked ~ .ytlv-switch-thumb { transform: translateX(16px); }
/* Footer */
#yt-lv-panel-footer { padding: 8px 16px 14px; display: flex; flex-direction: column; gap: 6px; }
#yt-lv-cache-stats { font-size: 11px; color: var(--ytlv-text-muted, #717171); text-align: center; }
.ytlv-reset-btn {
width: 100%; padding: 9px 16px;
background: transparent;
border: 1px solid var(--ytlv-reset-border, rgba(255,255,255,0.12));
color: var(--ytlv-text-secondary, #aaa);
border-radius: var(--ytlv-radius-full, 100px);
cursor: pointer;
font-size: 13px; font-family: "Roboto","Arial",sans-serif; font-weight: 500;
letter-spacing: 0.01em; transition: all 0.15s;
}
.ytlv-reset-btn:hover {
background: var(--ytlv-reset-hover-bg, rgba(255,255,255,0.08));
border-color: var(--ytlv-reset-hover-border, rgba(255,255,255,0.3));
color: var(--ytlv-text-primary, #f1f1f1);
}
.ytlv-reset-btn:active { background: var(--ytlv-reset-active-bg, rgba(255,255,255,0.14)); }
`);
// ══════════════════════════════════════════════════════════════════
// STATE
// ══════════════════════════════════════════════════════════════════
let listViewEnabled = true;
let layoutFixDone = false;
let lastUrl = location.href;
let rafPending = false;
let watchLaterDone = false;
let isSliderDragging = false;
let _sliderRaf = 0;
const _pendingCSS = {};
// WeakSets — skip already-processed items on repeat calls
let _processedHeaders = new WeakSet();
let _processedAvatars = new WeakSet();
// Targeted MutationObserver (narrower than body)
let _feedMo = null;
let _feedMoTarget = null;
// ══════════════════════════════════════════════════════════════════
// VIEW MODE CONTROL
// ══════════════════════════════════════════════════════════════════
function setListView(enabled) {
listViewEnabled = enabled;
document.documentElement.classList.toggle('list-view-active', enabled);
updateToggleUI();
if (!enabled) stopShimmer();
if (enabled && isTargetPage()) {
injectLayoutFix();
scheduleUpdate();
}
}
function applySettings(s) {
settings = { ...DEFAULT_SETTINGS, ...s };
const root = document.documentElement;
for (const { key, unit, cssVar } of SLIDER_CONFIGS) {
root.style.setProperty(cssVar, settings[key] + unit);
}
root.classList.toggle('hide-dividers', !!settings.hideDividers);
root.classList.toggle('hide-shorts', !!settings.hideShorts);
root.classList.toggle('show-descriptions', !!settings.showDescriptions);
applyPanelTransparency(!!settings.panelTransparent);
applyPanelLight(!!settings.panelLight);
setListView(settings.viewModeSubs !== 'grid');
updateShortsScroll();
processMostRelevant();
}
function applyPanelTransparency(enabled) {
document.getElementById('yt-lv-panel')?.classList.toggle('panel-transparent', enabled);
}
function applyPanelLight(enabled) {
document.getElementById('yt-lv-panel')?.classList.toggle('panel-light', enabled);
}
function scheduleUpdate() {
if (rafPending) return;
rafPending = true;
requestAnimationFrame(() => { rafPending = false; handleDomChanges(); });
}
// ══════════════════════════════════════════════════════════════════
// SMOOTH SLIDER HELPERS
// ══════════════════════════════════════════════════════════════════
function flushPendingCSS() {
if (_sliderRaf) { cancelAnimationFrame(_sliderRaf); _sliderRaf = 0; }
const root = document.documentElement;
for (const [prop, val] of Object.entries(_pendingCSS)) root.style.setProperty(prop, val);
for (const k in _pendingCSS) delete _pendingCSS[k];
}
function liveApply(key, value) {
settings[key] = value;
const cfg = SLIDER_CONFIGS.find(c => c.key === key);
if (!cfg) return;
_pendingCSS[cfg.cssVar] = value + cfg.unit;
if (!_sliderRaf) {
_sliderRaf = requestAnimationFrame(() => {
const root = document.documentElement;
for (const [prop, val] of Object.entries(_pendingCSS)) root.style.setProperty(prop, val);
for (const k in _pendingCSS) delete _pendingCSS[k];
_sliderRaf = 0;
});
}
}
document.addEventListener('pointerup', () => { isSliderDragging = false; }, true);
// ══════════════════════════════════════════════════════════════════
// SVG ICON PATHS
// ══════════════════════════════════════════════════════════════════
const ICON_SETTINGS = 'M19.14 12.94c.04-.3.06-.61.06-.94s-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z';
const ICON_CLOSE = 'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
const ICON_SUN = 'M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 00-1.41 0 .996.996 0 000 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 00-1.41 0 .996.996 0 000 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 000-1.41l-1.06-1.06zm1.06-12.37l-1.06 1.06a.996.996 0 000 1.41c.39.39 1.03.39 1.41 0l1.06-1.06a.996.996 0 000-1.41-.996.996 0 00-1.41 0zM7.05 18.36l-1.06 1.06a.996.996 0 000 1.41c.39.39 1.03.39 1.41 0l1.06-1.06a.996.996 0 000-1.41-.99.99 0 00-1.41 0z';
const ICON_MOON = 'M12 3a9 9 0 109 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 01-4.4 2.26 5.403 5.403 0 01-3.14-9.8c-.44-.06-.9-.1-1.36-.1z';
// ══════════════════════════════════════════════════════════════════
// SETTINGS PANEL
// ══════════════════════════════════════════════════════════════════
function buildPanel() {
if (document.getElementById('yt-lv-panel')) return;
const overlay = el('div', { id: 'yt-lv-panel-overlay' });
overlay.addEventListener('click', closePanel);
const panel = el('div', { id: 'yt-lv-panel' });
panel.addEventListener('click', e => e.stopPropagation());
// ── Header ──
const settingsIcon = makeSVGIcon(ICON_SETTINGS, 20);
settingsIcon.id = 'yt-lv-panel-header-icon';
const closeIcon = makeSVGIcon(ICON_CLOSE, 18);
const closeBtn = el('button', { id: 'yt-lv-panel-close', title: 'Close (Esc)' }, [closeIcon]);
closeBtn.addEventListener('click', closePanel);
const sunIcon = makeSVGIcon(ICON_SUN, 18); sunIcon.classList.add('icon-sun');
const moonIcon = makeSVGIcon(ICON_MOON, 18); moonIcon.classList.add('icon-moon');
const themeBtn = el('button', { id: 'yt-lv-theme-btn', title: 'Toggle light/dark panel' }, [sunIcon, moonIcon]);
themeBtn.addEventListener('click', () => {
const p = document.getElementById('yt-lv-panel');
const isLight = p.classList.toggle('panel-light');
settings.panelLight = isLight;
setSetting('panelLight', isLight);
});
const header = el('div', { id: 'yt-lv-panel-header' }, [
settingsIcon,
el('span', { id: 'yt-lv-panel-title', textContent: 'List View' }),
el('span', { id: 'yt-lv-panel-shortcut', textContent: 'Alt+L' }),
themeBtn,
closeBtn,
]);
panel.appendChild(header);
const body = el('div', { id: 'yt-lv-panel-body' });
// ── View Mode Chips ──
body.appendChild(el('div', { className: 'ytlv-section-label', textContent: 'View mode' }));
const gridChip = el('div', { id: 'lv-icon-grid', className: 'ytlv-chip', title: 'Grid View' });
const gridSvg = createSVG(GRID_RECTS);
gridSvg.setAttribute('width', '16'); gridSvg.setAttribute('height', '16');
gridChip.append(gridSvg, document.createTextNode('Grid'));
gridChip.addEventListener('click', () => {
settings.viewModeSubs = 'grid'; setSetting('viewModeSubs', 'grid');
setListView(false); updatePanelChips();
});
const listChip = el('div', { id: 'lv-icon-list', className: 'ytlv-chip', title: 'List View' });
const listSvg = createSVG(LIST_RECTS);
listSvg.setAttribute('width', '16'); listSvg.setAttribute('height', '16');
listChip.append(listSvg, document.createTextNode('List'));
listChip.addEventListener('click', () => {
settings.viewModeSubs = 'list'; setSetting('viewModeSubs', 'list');
setListView(true); updatePanelChips();
});
body.appendChild(el('div', { id: 'ytlv-view-chips' }, [gridChip, listChip]));
body.appendChild(el('div', { className: 'ytlv-divider' }));
// ── Sliders ──
body.appendChild(el('div', { className: 'ytlv-section-label', textContent: 'Layout' }));
for (const { key, label, min, max } of SLIDER_CONFIGS) {
const slider = el('input', { type: 'range', id: `lv-${key}-slider`, min, max, step: '1' });
const numInput = el('input', { type: 'number', id: `lv-${key}`, min, max, step: '1', className: 'ytlv-num-input' });
slider.addEventListener('pointerdown', () => { isSliderDragging = true; });
slider.addEventListener('input', () => { numInput.value = slider.value; liveApply(key, +slider.value); });
slider.addEventListener('change', () => { flushPendingCSS(); isSliderDragging = false; setSetting(key, +slider.value); });
numInput.addEventListener('input', () => {
const v = +numInput.value;
if (v >= min && v <= max) { slider.value = v; liveApply(key, v); }
});
numInput.addEventListener('change', () => {
const v = Math.min(max, Math.max(min, +numInput.value));
numInput.value = v; slider.value = v;
flushPendingCSS(); setSetting(key, v); liveApply(key, v);
});
body.appendChild(el('div', { className: 'ytlv-slider-row' }, [
el('span', { className: 'ytlv-slider-label', textContent: label }),
el('div', { className: 'ytlv-slider-track' }, [slider, numInput]),
]));
}
body.appendChild(el('div', { className: 'ytlv-divider' }));
// ── Toggles ──
body.appendChild(el('div', { className: 'ytlv-section-label', textContent: 'Options' }));
for (const { key, label, warn } of TOGGLE_CONFIGS) {
const cbInput = el('input', { type: 'checkbox', id: `lv-${key}` });
const track = el('span', { className: 'ytlv-switch-track' });
const thumb = el('span', { className: 'ytlv-switch-thumb' });
const sw = el('label', { className: 'ytlv-switch', htmlFor: `lv-${key}` }, [cbInput, track, thumb]);
cbInput.addEventListener('change', () => {
setSetting(key, cbInput.checked);
settings[key] = cbInput.checked;
if (key === 'showDescriptions') {
document.documentElement.classList.toggle('show-descriptions', cbInput.checked);
if (cbInput.checked) { processItems(); ensureDescLoop(); }
else { clearDescriptions(); stopShimmer(); }
updateCacheStats();
} else if (key === 'hideShorts') {
document.documentElement.classList.toggle('hide-shorts', cbInput.checked);
} else if (key === 'panelTransparent') {
applyPanelTransparency(cbInput.checked);
} else {
applySettings(getAllSettings());
}
});
body.appendChild(el('div', { className: 'ytlv-toggle-row' }, [
el('label', { className: 'ytlv-toggle-label', htmlFor: `lv-${key}`, textContent: label }),
sw,
]));
if (warn) {
body.appendChild(el('div', { className: 'ytlv-toggle-warn', textContent: warn }));
}
}
// ── Footer ──
const statsLine = el('div', { id: 'yt-lv-cache-stats' });
const resetBtn = el('button', { className: 'ytlv-reset-btn', textContent: 'Reset to defaults' });
resetBtn.addEventListener('click', () => {
for (const [k, v] of Object.entries(DEFAULT_SETTINGS)) setSetting(k, v);
clearDescriptions();
applySettings(getAllSettings());
loadPanelValues();
updateCacheStats();
});
const footer = el('div', { id: 'yt-lv-panel-footer' }, [statsLine, resetBtn]);
panel.append(body, el('div', { className: 'ytlv-divider' }), footer);
document.documentElement.append(overlay, panel);
makeDraggable(panel, header);
applyPanelTransparency(!!settings.panelTransparent);
applyPanelLight(!!settings.panelLight);
}
function updateCacheStats() {
const statsEl = document.getElementById('yt-lv-cache-stats');
if (!statsEl) return;
descStoreLoad();
const count = Object.keys(_descState.persist || {}).length;
statsEl.textContent = count > 0 ? `${count} descriptions cached` : '';
}
function openPanel() {
buildPanel(); loadPanelValues(); updateCacheStats();
document.getElementById('yt-lv-panel-overlay').classList.add('open');
document.getElementById('yt-lv-panel').classList.add('open');
}
function closePanel() {
document.getElementById('yt-lv-panel-overlay')?.classList.remove('open');
document.getElementById('yt-lv-panel')?.classList.remove('open');
}
function togglePanel() {
document.getElementById('yt-lv-panel')?.classList.contains('open') ? closePanel() : openPanel();
}
function loadPanelValues() {
const s = getAllSettings();
for (const { key } of SLIDER_CONFIGS) {
const slider = document.getElementById(`lv-${key}-slider`);
const number = document.getElementById(`lv-${key}`);
if (slider) slider.value = s[key];
if (number) number.value = s[key];
}
for (const { key } of TOGGLE_CONFIGS) {
const cb = document.getElementById(`lv-${key}`);
if (cb) cb.checked = s[key];
}
updatePanelChips();
}
function updatePanelChips() {
const isGrid = settings.viewModeSubs === 'grid';
document.getElementById('lv-icon-grid')?.classList.toggle('active', isGrid);
document.getElementById('lv-icon-list')?.classList.toggle('active', !isGrid);
}
function makeDraggable(panel, handle) {
handle.addEventListener('mousedown', e => {
if (e.target.closest('button')) return;
e.preventDefault();
const rect = panel.getBoundingClientRect();
const [sx, sy, sl, st] = [e.clientX, e.clientY, rect.left, rect.top];
const onMove = ev => {
panel.style.left = (sl + ev.clientX - sx) + 'px';
panel.style.top = (st + ev.clientY - sy) + 'px';
panel.style.right = 'auto';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
}
GM_registerMenuCommand('⚙️ Open Settings Panel (Alt+L)', openPanel);
// ══════════════════════════════════════════════════════════════════
// KEYBOARD SHORTCUT
// ══════════════════════════════════════════════════════════════════
document.addEventListener('keydown', e => {
const tag = e.target.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || e.target.isContentEditable) return;
if (e.altKey && !e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === 'l') {
e.preventDefault(); e.stopPropagation(); togglePanel();
}
if (e.key === 'Escape') closePanel();
}, true);
// ══════════════════════════════════════════════════════════════════
// TOGGLE BUTTONS
// ══════════════════════════════════════════════════════════════════
function injectToggleButtons() {
if (!isSubs()) return;
let container = document.getElementById('view-toggle-container');
if (!container) {
container = el('div', { id: 'view-toggle-container' });
const makeBtn = (id, title, rects, mode, labelText) => {
const svg = createSVG(rects);
svg.setAttribute('width', '16'); svg.setAttribute('height', '16');
const label = el('span', { className: 'vt-label', textContent: labelText });
const btn = el('button', { className: 'view-toggle-btn', id, title }, [svg, label]);
btn.onclick = e => {
e.stopPropagation();
settings.viewModeSubs = mode;
setSetting('viewModeSubs', mode);
setListView(mode === 'list');
updatePanelChips();
};
return btn;
};
container.append(
makeBtn('toggle-grid-btn', 'Grid View', GRID_RECTS, 'grid', 'Grid'),
makeBtn('toggle-list-btn', 'List View', LIST_RECTS, 'list', 'List'),
);
}
const subBtn = document.querySelector('ytd-shelf-renderer #subscribe-button');
const target = subBtn?.parentElement;
if (target && container.parentElement !== target) {
target.appendChild(container);
updateToggleUI();
}
}
function updateToggleUI() {
document.getElementById('toggle-list-btn')?.classList.toggle('active', listViewEnabled);
document.getElementById('toggle-grid-btn')?.classList.toggle('active', !listViewEnabled);
}
// ══════════════════════════════════════════════════════════════════
// ITEM PROCESSING
// ══════════════════════════════════════════════════════════════════
function processItemHeader(item) {
if (!_processedHeaders.has(item)) {
const lockup = item.querySelector('.yt-lockup-view-model');
const meta = item.querySelector('yt-content-metadata-view-model');
const META_ROW = ':is(.yt-content-metadata-view-model__metadata-row, .ytContentMetadataViewModelMetadataRow)';
const row = meta?.querySelector(META_ROW);
if (lockup && row) {
const clone = row.cloneNode(true);
clone.classList.add('cloned-channel-name');
clone.querySelectorAll(':is(.yt-content-metadata-view-model__delimiter, .ytContentMetadataViewModelDelimiter)').forEach(n => n.remove());
clone.querySelectorAll(':is(.yt-content-metadata-view-model__leading-icon, .ytContentMetadataViewModelLeadingIcon)').forEach(n => n.remove());
clone.querySelectorAll(':is(.yt-content-metadata-view-model__metadata-text, .ytContentMetadataViewModelMetadataText)').forEach(span => {
if (!span.querySelector('a')) span.remove();
});
lockup.appendChild(clone);
_processedHeaders.add(item);
}
}
if (!_processedAvatars.has(item)) {
const avatar = item.querySelector('.yt-lockup-metadata-view-model__avatar');
if (avatar && !avatar.querySelector('.custom-avatar-link')) {
const link = item.querySelector('.cloned-channel-name a, yt-content-metadata-view-model a');
if (link) {
const anchor = el('a', { href: link.href, className: 'custom-avatar-link' });
while (avatar.firstChild) anchor.appendChild(avatar.firstChild);
avatar.appendChild(anchor);
_processedAvatars.add(item);
}
}
}
}
function ensureDescriptionEl(item) {
if (!settings.showDescriptions || !listViewEnabled) return;
const metaContainer = item.querySelector('yt-content-metadata-view-model');
if (!metaContainer) return;
const linkEl = item.querySelector('a#video-title-link, a.yt-lockup-metadata-view-model__title');
if (!linkEl) return;
const vid = extractVideoId(linkEl.href);
if (!vid) return;
// Ensure the DOM element exists with a skeleton placeholder
let descEl = metaContainer.querySelector('.custom-description');
if (!descEl) {
descEl = el('div', { className: 'custom-description' });
metaContainer.appendChild(descEl);
}
if (descEl.dataset.vid !== vid) {
descEl.dataset.vid = vid;
descEl.textContent = '';
descEl.style.display = '';
descEl.classList.add('desc-skeleton');
}
// Kick off the fetch (no-ops if cached or already in flight)
fetchDescription(item);
}
function processItems() {
for (const item of document.querySelectorAll('ytd-rich-item-renderer')) {
if (item.querySelector('ytd-ad-slot-renderer')) { item.remove(); continue; }
processItemHeader(item);
ensureDescriptionEl(item);
}
}
function clearDescriptions() {
for (const div of document.querySelectorAll('.custom-description')) {
div.remove();
}
}
// ══════════════════════════════════════════════════════════════════
// WATCH LATER SIDEBAR LINK
// ══════════════════════════════════════════════════════════════════
function injectWatchLater() {
if (watchLaterDone || !listViewEnabled) return;
if (document.querySelector('a[href="/playlist?list=WL"]')) { watchLaterDone = true; return; }
const ref = ['a[href="/feed/playlists"]', 'a[href="/feed/history"]']
.map(s => document.querySelector(s)?.closest('ytd-guide-entry-renderer'))
.find(Boolean);
if (!ref?.parentElement) return;
const NS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(NS, 'svg');
for (const [k, v] of Object.entries({ xmlns: NS, height: '24', viewBox: '0 0 24 24', width: '24', focusable: 'false', 'aria-hidden': 'true' })) svg.setAttribute(k, v);
svg.style.cssText = 'pointer-events:none;display:inherit;width:100%;height:100%;';
const path = document.createElementNS(NS, 'path');
path.setAttribute('d', 'M12 1C5.925 1 1 5.925 1 12s4.925 11 11 11 11-4.925 11-11S18.075 1 12 1Zm0 2a9 9 0 110 18.001A9 9 0 0112 3Zm0 3a1 1 0 00-1 1v5.565l.485.292 3.33 2a1 1 0 001.03-1.714L13 11.435V7a1 1 0 00-1-1Z');
svg.appendChild(path);
const entry = el('ytd-guide-entry-renderer', {
className: 'style-scope ytd-guide-collapsible-section-entry-renderer', 'line-end-style': 'none',
}, [
el('a', { id: 'endpoint', className: 'yt-simple-endpoint style-scope ytd-guide-entry-renderer', tabindex: '-1', role: 'link', href: '/playlist?list=WL', title: 'Watch later' }, [
el('tp-yt-paper-item', { role: 'link', className: 'style-scope ytd-guide-entry-renderer', 'style-target': 'host', tabindex: '0', 'aria-disabled': 'false' }, [
el('yt-icon', { className: 'guide-icon style-scope ytd-guide-entry-renderer' }, [
el('span', { className: 'yt-icon-shape style-scope yt-icon ytSpecIconShapeHost' }, [
el('div', { style: { width: '100%', height: '100%', display: 'block', fill: 'currentcolor' } }, [svg]),
]),
]),
el('yt-formatted-string', { className: 'title style-scope ytd-guide-entry-renderer', textContent: 'Watch later' }),
]),
]),
]);
ref.after(entry);
watchLaterDone = true;
}
// ══════════════════════════════════════════════════════════════════
// LAYOUT FIX
// ══════════════════════════════════════════════════════════════════
function injectLayoutFix() {
if (layoutFixDone || !listViewEnabled) return;
const primary = document.querySelector('ytd-two-column-browse-results-renderer #primary');
if (!primary) return;
layoutFixDone = true;
const dummy = el('div', { style: { width: '1px', height: '100vh', flexShrink: '0', background: 'transparent', transition: 'width 0.2s ease-out' } });
const origDisplay = primary.style.display;
primary.style.display = 'flex'; primary.style.flexDirection = 'row';
primary.prepend(dummy);
setTimeout(() => {
dummy.style.width = '0px';
setTimeout(() => { dummy.remove(); primary.style.display = origDisplay; dispatchEvent(new Event('resize')); }, 200);
}, SHIFT_DELAY);
}
// ══════════════════════════════════════════════════════════════════
// FEATURE TOGGLES
// ══════════════════════════════════════════════════════════════════
function processMostRelevant() {
if (!settings.hideMostRelevant) {
for (const s of document.querySelectorAll('ytd-rich-section-renderer[data-hidden-by-ext="true"]')) {
s.style.display = ''; s.removeAttribute('data-hidden-by-ext');
}
return;
}
for (const s of document.querySelectorAll('ytd-rich-section-renderer:not([data-hidden-by-ext="true"])')) {
if (s.querySelector('span#title')?.textContent?.trim().toLowerCase() === 'most relevant') {
s.style.display = 'none'; s.setAttribute('data-hidden-by-ext', 'true');
}
}
}
function handleShortsScroll(e) {
const btnRenderer = e.target.closest('.expand-collapse-button');
if (!btnRenderer) return;
const isLess =
(btnRenderer.querySelector('button')?.getAttribute('aria-label') || '').toLowerCase() === 'show less' ||
(btnRenderer.querySelector('.yt-core-attributed-string')?.textContent?.trim() || '').toLowerCase() === 'show less';
if (!isLess) return;
const shelf = btnRenderer.closest('ytd-rich-shelf-renderer');
if (shelf?.getBoundingClientRect().top < 56) {
setTimeout(() => scrollTo({ top: shelf.getBoundingClientRect().top + scrollY - 72, behavior: 'smooth' }), 500);
}
}
function updateShortsScroll() {
document.removeEventListener('click', handleShortsScroll);
if (settings.changeShortsScroll) document.addEventListener('click', handleShortsScroll);
}
function ensurePageSubtype() {
const browse = document.querySelector('ytd-browse');
if (browse && browse.getAttribute('page-subtype') !== 'subscriptions') {
browse.setAttribute('page-subtype', 'subscriptions');
}
}
// ══════════════════════════════════════════════════════════════════
// NAV RESET
// ══════════════════════════════════════════════════════════════════
function resetNavState() {
_processedHeaders = new WeakSet();
_processedAvatars = new WeakSet();
_descState.inFlight.clear();
_descState.memCache.clear();
layoutFixDone = false;
watchLaterDone = false;
// Re-attach observer to new feed DOM after navigation
_feedMoTarget = null;
}
// ══════════════════════════════════════════════════════════════════
// MAIN DOM HANDLER
// ══════════════════════════════════════════════════════════════════
function handleDomChanges() {
if (location.href !== lastUrl) {
lastUrl = location.href; resetNavState();
}
injectToggleButtons();
if (!isTargetPage()) return;
ensurePageSubtype();
attachFeedObserver();
if (listViewEnabled) {
processItems();
if (!watchLaterDone) injectWatchLater();
if (!layoutFixDone && document.querySelector('ytd-rich-item-renderer')) injectLayoutFix();
}
processMostRelevant();
}
// ══════════════════════════════════════════════════════════════════
// OBSERVERS & INIT
// ══════════════════════════════════════════════════════════════════
// Broad observer on body — only used as a fallback for navigation detection
// and to trigger the targeted observer attachment.
new MutationObserver(() => {
if (!isSliderDragging) scheduleUpdate();
}).observe(document.body, { childList: true, subtree: true });
// Targeted observer: watches only the feed contents container.
// Much cheaper than observing all of document.body.
function attachFeedObserver() {
if (!isTargetPage()) return;
const target =
document.querySelector('ytd-rich-grid-renderer #contents') ||
document.querySelector('ytd-rich-grid-renderer');
if (!target || target === _feedMoTarget) return;
_feedMo?.disconnect();
_feedMoTarget = target;
_feedMo = new MutationObserver(muts => {
if (isSliderDragging) return;
for (const m of muts)
for (const node of m.addedNodes)
if (node?.nodeType === 1 && node.tagName === 'YTD-RICH-ITEM-RENDERER')
scheduleUpdate();
});
_feedMo.observe(target, { childList: true });
}
document.addEventListener('yt-navigate-finish', () => {
lastUrl = location.href;
resetNavState();
applySettings(settings);
handleDomChanges();
attachFeedObserver();
});
window.addEventListener('popstate', () => {
resetNavState();
handleDomChanges();
}, { passive: true });
// Boot
descStoreLoad();
applySettings(getAllSettings());
ensureDescLoop();
setTimeout(() => attachFeedObserver(), 300);
})();