YouTube Enhancer (Simple)

YouTube 增强:极简单图预览、D键开关、修复频道名、自动跳过 Shorts、J/K 导航、空格新标签页。

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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);
})();