YouTube 增强:极简单图预览、D键开关、修复频道名、自动跳过 Shorts、J/K 导航、空格新标签页。
// ==UserScript==
// @name YouTube Enhancer (Simple)
// @namespace http://tampermonkey.net/
// @version 2.4
// @description YouTube 增强:极简单图预览、D键开关、修复频道名、自动跳过 Shorts、J/K 导航、空格新标签页。
// @author Antigravity
// @match *://www.youtube.com
// @grant GM_download
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const SITE_CONFIG = {
name: "YouTube",
selectors: {
item: 'ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer',
author: '#channel-name #text, ytd-channel-name #text, .yt-lockup-metadata-view-model__metadata a[href*="/@"], #byline-container a',
postLink: 'a#video-title-link, a#video-title, .yt-lockup-metadata-view-model__title, .yt-lockup-view-model__content-image',
contentSpans: '#video-title, #video-title-link, .yt-lockup-metadata-view-model__title',
media: { img: 'yt-image img, .yt-core-image, img' }
},
theme: { accent: '#ff0000', overlayBg: 'rgba(0,0,0,0.96)', borderRadius: '16px' }
};
let policy = { createHTML: (s) => s };
if (window.trustedTypes?.createPolicy) {
try { policy = window.trustedTypes.createPolicy('enhancer-policy', { createHTML: (s) => s }); } catch (e) { }
}
const setHTML = (el, html) => {
if (!el) return;
try { el.innerHTML = policy.createHTML(html); } catch (e) { el.textContent = html.replace(/<[^>]*>/g, ''); }
};
const App_Core = (() => {
return {
getPostData: (card) => {
if (!card) return null;
const linkEl = card.querySelector(SITE_CONFIG.selectors.postLink);
const titleEl = card.querySelector(SITE_CONFIG.selectors.contentSpans);
const authorEl = card.querySelector(SITE_CONFIG.selectors.author);
const avatarImg = card.querySelector('yt-img-shadow#avatar img, #avatar img, .yt-spec-avatar-shape__image');
const mainImg = card.querySelector(SITE_CONFIG.selectors.media.img);
return {
row: card,
url: linkEl?.href || null,
content: titleEl?.innerText.trim() || titleEl?.title || "",
author: authorEl?.title || authorEl?.innerText.trim() || "Channel",
avatarUrl: avatarImg?.src || avatarImg?.getAttribute('src'),
src: mainImg?.src || mainImg?.getAttribute('src')
};
},
findComments: () => {
const all = Array.from(document.querySelectorAll(SITE_CONFIG.selectors.item));
return all.filter(c => {
// 核心过滤:排除嵌套关系,确保每个评论只抓取一次最外层
let p = c.parentElement;
while (p) { if (p.matches?.(SITE_CONFIG.selectors.item)) return false; p = p.parentElement; }
return c.offsetHeight > 20;
});
},
findCards: () => {
const all = Array.from(document.querySelectorAll(SITE_CONFIG.selectors.item));
return all.filter(c => {
if (c.hasAttribute('is-slim-media') || c.querySelector('ytm-shorts-lockup-view-model')) return false;
const link = c.querySelector(SITE_CONFIG.selectors.postLink);
if (link && link.href && link.href.includes('/shorts/')) return false;
// 核心过滤:排除嵌套关系
let p = c.parentElement;
while (p) { if (p.matches?.(SITE_CONFIG.selectors.item)) return false; p = p.parentElement; }
return c.offsetHeight > 50;
});
}
};
})();
const App_UI = (() => {
let overlay, vImg, vText, vInfo, vAvatar;
let isEnabled = true;
const injectCSS = () => {
if (document.getElementById('enhancer-css')) return;
const s = document.createElement('style'); s.id = 'enhancer-css';
s.textContent = `
.eh-active { outline: 4px solid ${SITE_CONFIG.theme.accent} !important; outline-offset: 0px !important; border-radius: 12px !important; z-index: 10 !important; position: relative !important; }
.eh-main-image { width: 100%; height: auto; object-fit: contain; border-radius: 12px; margin-bottom: 25px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
.eh-avatar { width: 60px; height: 60px; border-radius: 50%; border: 3px solid rgba(255,255,255,0.2); object-fit: cover; }
.eh-author { font-size: 24px; font-weight: 900; color: #fff; }
.eh-title { font-size: 26px; font-weight: 700; line-height: 1.4; color: rgba(255,255,255,0.9); margin-top: 10px; }
`;
document.head.appendChild(s);
};
const update = (data, idx, total) => {
if (!data) return;
vImg.src = data.src || '';
// 头部:头像 + 作者 (水平排列)
setHTML(vAvatar, `
<div style="display:flex; align-items:center; gap:20px; margin-bottom:15px;">
<img src="${data.avatarUrl || ''}" class="eh-avatar">
<div class="eh-author">${data.author}</div>
</div>
`);
// 视频标题
setHTML(vText, `<div class="eh-title">${data.content}</div>`);
// 底部布局:左(链接) 中(提示) 右(滑块)
setHTML(vInfo, `
<div style="border-top:1px solid rgba(255,255,255,0.1); padding-top:15px; margin-top:10px; display:flex; align-items:center; justify-content:space-between; width:100%;">
<!-- 左下:更新地址 -->
<div style="flex:1; text-align:left;">
<a href="https://greasyfork.org/zh-CN/scripts/564767" target="_blank" style="color:rgba(255,255,255,0.2); font-size:10px; text-decoration:none; transition:color 0.2s;" onmouseover="this.style.color='rgba(255,255,255,0.5)'" onmouseout="this.style.color='rgba(255,255,255,0.2)'">GreasyFork / Info</a>
</div>
<!-- 中间:提示 -->
<div style="flex:1.5; text-align:center; color:rgba(255,255,255,0.4); font-size:11px; font-weight:500; display:flex; align-items:center; justify-content:center; gap:20px;">
<span>Pro Navigation Enabled</span>
<span><b>[SPACE]</b> 新标签页</span>
<span><b>[D]</b> 关闭</span>
</div>
<!-- 右下:卡片宽度 -->
<div style="flex:1; display:flex; align-items:center; justify-content:flex-end; gap:10px;">
<span style="font-size:10px; color:rgba(255,255,255,0.3); white-space:nowrap;">宽度</span>
<input type="range" id="eh-size-slider" min="400" max="1400" step="50" value="${GM_getValue('eh_card_width', 800)}" style="width:80px; cursor:pointer; accent-color:#ffffff00;">
</div>
</div>
`);
// counter 已移除
// 重新绑定宽度调节和设置
const slider = document.getElementById('eh-size-slider');
const card = vImg.parentElement;
if (slider && card) {
const updateWidth = (val) => { card.style.width = `${val}px`; GM_setValue('eh_card_width', val); };
slider.oninput = (e) => updateWidth(e.target.value);
updateWidth(slider.value);
}
};
return {
show: (data, idx, total) => {
if (!isEnabled) return;
if (!overlay) {
overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.85)',
zIndex: 999999,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
});
const card = document.createElement('div');
Object.assign(card.style, {
width: GM_getValue('eh_card_width', 800) + 'px',
maxWidth: '90vw',
maxHeight: '90vh',
background: 'rgba(255,255,255,0.05)',
backdropFilter: 'blur(20px) saturate(180%)',
borderRadius: '30px',
padding: '40px',
position: 'relative',
boxShadow: '0 40px 100px rgba(0,0,0,0.8)',
border: '1px solid rgba(255,255,255,0.1)',
display: 'flex',
flexDirection: 'column',
overflowX: 'hidden',
overflowY: 'auto'
});
vImg = document.createElement('img'); vImg.className = 'eh-main-image';
vAvatar = document.createElement('div');
vText = document.createElement('div');
vInfo = document.createElement('div');
card.append(vImg, vAvatar, vText, vInfo); overlay.append(card); document.body.appendChild(overlay);
overlay.onclick = (e) => { if (e.target === overlay) overlay.style.display = 'none'; };
}
overlay.style.display = 'flex';
update(data, idx, total);
},
hide: () => { if (overlay) overlay.style.display = 'none'; },
toggle: () => { isEnabled = !isEnabled; if (!isEnabled) App_UI.hide(); App_UI.showToast(isEnabled ? '预览:开启' : '预览:关闭'); return isEnabled; },
isVisible: () => overlay && overlay.style.display === 'flex' && isEnabled,
injectCSS,
showToast: (msg) => {
const old = document.getElementById('enhancer-toast'); if (old) old.remove();
const t = document.createElement('div'); t.id = 'enhancer-toast';
Object.assign(t.style, { position: 'fixed', bottom: '50px', left: '50%', transform: 'translateX(-50%)', padding: '12px 30px', background: '#fff', color: '#000', borderRadius: '30px', zIndex: 1000005, fontWeight: '900', boxShadow: '0 15px 30px rgba(0,0,0,0.4)' });
t.innerText = msg; document.body.appendChild(t);
setTimeout(() => { t.style.opacity = '0'; t.style.transition = '0.3s'; setTimeout(() => t.remove(), 500); }, 1500);
}
};
})();
const App_Nav = (() => {
let curIdx = -1, cards = [];
return {
move: (direction) => {
cards = App_Core.findCards();
if (!cards.length) return null;
// 首次按下,寻找中心卡片
if (curIdx === -1 || !cards[curIdx]) {
const centerTop = window.innerHeight / 2;
const sorted = [...cards].sort((a, b) => {
const ra = a.getBoundingClientRect();
const rb = b.getBoundingClientRect();
return Math.abs(ra.top + ra.height / 2 - centerTop) - Math.abs(rb.top + rb.height / 2 - centerTop);
});
curIdx = cards.indexOf(sorted[0]);
return { data: App_Core.getPostData(cards[curIdx]), index: curIdx, total: cards.length };
}
if (direction === 'left') {
curIdx = Math.max(0, curIdx - 1);
} else if (direction === 'right') {
curIdx = Math.min(cards.length - 1, curIdx + 1);
} else {
// 上下移动:空间几何查找
const rect = cards[curIdx].getBoundingClientRect();
const curX = rect.left + rect.width / 2;
let bestIdx = -1;
let minDiff = Infinity;
for (let i = 0; i < cards.length; i++) {
if (i === curIdx) continue;
const r = cards[i].getBoundingClientRect();
const targetX = r.left + r.width / 2;
const isBelow = r.top > rect.top + 10;
const isAbove = r.bottom < rect.bottom - 10;
if ((direction === 'down' && isBelow) || (direction === 'up' && isAbove)) {
const dy = Math.abs(r.top - rect.top);
const dx = Math.abs(targetX - curX);
const score = dy + dx * 2.8; // YouTube 权重稍微调高,确保垂直对齐
if (score < minDiff) {
minDiff = score;
bestIdx = i;
}
}
}
if (bestIdx !== -1) curIdx = bestIdx;
}
return { data: App_Core.getPostData(cards[curIdx]), index: curIdx, total: cards.length };
},
scroll: () => {
document.querySelectorAll('.eh-active').forEach(e => e.classList.remove('eh-active'));
const c = cards[curIdx];
if (c) { c.classList.add('eh-active'); c.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
},
reset: () => { curIdx = -1; document.querySelectorAll('.eh-active').forEach(e => e.classList.remove('eh-active')); },
getCard: () => cards[curIdx]
};
})();
const init = () => {
window.addEventListener('keydown', (e) => {
if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) || document.activeElement.hasAttribute('contenteditable') || document.activeElement.id === 'search') return;
const k = e.key.toLowerCase();
if (k === 'd') {
const enabled = App_UI.toggle();
if (enabled) { const r = App_Nav.move(0); if (r) App_UI.show(r.data, r.index, r.total); }
return;
}
// 导航逻辑
let dir = '';
const isVisible = App_UI.isVisible();
if (k === 'j' || k === 'arrowdown') {
// 如果预览开着,简单的索引加 1 往往符合直觉,但为了统一,全部走空间导航
dir = 'down';
} else if (k === 'k' || k === 'arrowup') {
dir = 'up';
} else if (k === 'l' || k === 'arrowright') {
dir = 'right';
} else if (k === 'h' || k === 'arrowleft') {
dir = 'left';
}
if (dir) {
e.preventDefault();
const r = App_Nav.move(dir);
if (r) {
if (isVisible) App_UI.show(r.data, r.index, r.total);
App_Nav.scroll();
}
return;
}
if (e.code === 'Space') {
if (App_UI.isVisible() || !window.location.pathname.startsWith('/watch')) {
e.preventDefault(); e.stopPropagation();
const card = App_Nav.getCard();
if (card) { const data = App_Core.getPostData(card); if (data?.url) window.open(data.url, '_blank'); }
}
}
else if (k === 'escape' && App_UI.isVisible()) App_UI.hide();
}, true);
App_UI.injectCSS();
let lp = window.location.href; setInterval(() => { if (window.location.href !== lp) { lp = window.location.href; App_Nav.reset(); App_UI.hide(); } }, 800);
};
setTimeout(init, 2000);
})();