🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Youtube Enhancer By Domopremo

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.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey to install this script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

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