YouTube Comments Sidebar (Toggle, 2025)

Show comments in the right sidebar with a Comments/Recommended toggle on YouTube watch pages.

Verzia zo dňa 08.09.2025. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         YouTube Comments Sidebar (Toggle, 2025)
// @namespace    yt-comments-sidebar
// @version      1.3
// @description  Show comments in the right sidebar with a Comments/Recommended toggle on YouTube watch pages.
// @match        https://www.youtube.com/*
// @run-at       document-start
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  const IS_WATCH = () => location.pathname === "/watch" && /[?&]v=/.test(location.search);
  const QS = sel => document.querySelector(sel);

  const SELECTORS = {
    flexy: "ytd-watch-flexy",
    secondary: "ytd-watch-flexy #secondary",
    comments: "ytd-watch-flexy ytd-comments#comments, ytd-watch-flexy #comments",
  };

  const STYLE_ID = "tm-ycs-styles";
  const WRAP_ID = "tm-ycs-wrap";
  const TABS_ID = "tm-ycs-tabs";
  const UP_NEXT_ID = "tm-ycs-upnext";
  const COMMENTS_WRAP_ID = "tm-ycs-comments";

  function injectStyles() {
    if (QS(`#${STYLE_ID}`)) return;
    const s = document.createElement("style");
    s.id = STYLE_ID;
    s.textContent = `
      #${TABS_ID} {
        display:flex; gap:.5rem; align-items:center;
        position:sticky; top:56px; z-index:5;
        padding:.5rem .75rem; border-bottom:1px solid rgba(255,255,255,.08);
        background: rgba(18,18,18,.98);
        backdrop-filter: saturate(120%) blur(6px);
      }
      #${TABS_ID} button{
        font: 500 13px/1.2 system-ui, -apple-system, Segoe UI, Roboto, Arial;
        padding:.4rem .7rem; border-radius:999px; border:1px solid transparent; cursor:pointer;
        background:#272727; color:#eee;
      }
      #${TABS_ID} button.active{ background:#3a3a3a; border-color:#4a4a4a; }
      #${COMMENTS_WRAP_ID}, #${UP_NEXT_ID} {
        max-height: calc(100vh - 56px - 44px); /* header + tabs */
        overflow-y: auto; overflow-x: hidden;
      }
      /* Make comments readable in the narrow column */
      #${COMMENTS_WRAP_ID} #content { max-width: 100% !important; }
    `;
    document.documentElement.appendChild(s);
  }

  function waitFor(selector, { timeout = 15000, root = document } = {}) {
    return new Promise((resolve, reject) => {
      const found = root.querySelector(selector);
      if (found) return resolve(found);

      const obs = new MutationObserver(() => {
        const el = root.querySelector(selector);
        if (el) {
          obs.disconnect();
          resolve(el);
        }
      });
      obs.observe(root, { childList: true, subtree: true });

      setTimeout(() => {
        obs.disconnect();
        reject(new Error("Timeout waiting for " + selector));
      }, timeout);
    });
  }

  function buildTabs() {
    const tabs = document.createElement("div");
    tabs.id = TABS_ID;

    const btnComments = document.createElement("button");
    btnComments.textContent = "Comments";
    btnComments.dataset.target = COMMENTS_WRAP_ID;

    const btnUpNext = document.createElement("button");
    btnUpNext.textContent = "Recommended";
    btnUpNext.dataset.target = UP_NEXT_ID;

    tabs.append(btnComments, btnUpNext);

    function activate(targetId) {
      const commentsWrap = QS(`#${COMMENTS_WRAP_ID}`);
      const upnextWrap = QS(`#${UP_NEXT_ID}`);
      if (!commentsWrap || !upnextWrap) return;

      const buttons = tabs.querySelectorAll("button");
      buttons.forEach(b => b.classList.toggle("active", b.dataset.target === targetId));

      commentsWrap.style.display = (targetId === COMMENTS_WRAP_ID) ? "" : "none";
      upnextWrap.style.display     = (targetId === UP_NEXT_ID)     ? "" : "none";
    }

    tabs.addEventListener("click", (e) => {
      const b = e.target.closest("button");
      if (!b) return;
      activate(b.dataset.target);
    });

    // default to Comments first
    queueMicrotask(() => activate(COMMENTS_WRAP_ID));
    return tabs;
  }

  function wrapSecondary(secondary) {
    if (QS(`#${WRAP_ID}`)) return; // already wrapped

    // Move existing children into our UpNext wrapper
    const upNext = document.createElement("div");
    upNext.id = UP_NEXT_ID;

    const frag = document.createDocumentFragment();
    while (secondary.firstChild) frag.appendChild(secondary.firstChild);
    upNext.appendChild(frag);

    const commentsWrap = document.createElement("div");
    commentsWrap.id = COMMENTS_WRAP_ID;

    const container = document.createElement("div");
    container.id = WRAP_ID;

    const tabs = buildTabs();
    container.append(tabs, commentsWrap, upNext);
    secondary.appendChild(container);
  }

  function moveCommentsIntoSidebar() {
    const comments = QS(SELECTORS.comments);
    const slot = QS(`#${COMMENTS_WRAP_ID}`);
    if (!comments || !slot) return false;

    // Only move once
    if (!slot.contains(comments)) {
      slot.appendChild(comments);
    }
    return true;
  }

  function primeCommentsLoad() {
    // If YouTube hasn’t hydrated comments yet, nudge it by briefly scrolling
    // the original position into view (if it exists in DOM).
    const original = QS(SELECTORS.comments);
    if (original) {
      original.scrollIntoView({ block: "center" });
    }
  }

  async function initOnce() {
    if (!IS_WATCH()) return;

    injectStyles();

    const flexy = await waitFor(SELECTORS.flexy).catch(() => null);
    if (!flexy) return;

    const secondary = await waitFor(SELECTORS.secondary, { root: flexy }).catch(() => null);
    if (!secondary) return;

    wrapSecondary(secondary);
    primeCommentsLoad();

    // Keep trying to capture comments as soon as they appear
    let tries = 0;
    const maxTries = 40;
    const interval = setInterval(() => {
      const ok = moveCommentsIntoSidebar();
      tries++;
      if (ok || tries >= maxTries) clearInterval(interval);
    }, 500);
  }

  // Handle SPA navigation and first load
  const boot = () => {
    if (!IS_WATCH()) return;
    initOnce().catch(() => {});
  };

  // YouTube fires this on in-page navigation
  window.addEventListener("yt-navigate-finish", boot);
  // Also run after full load
  window.addEventListener("load", boot);
  // And if URL changes without event (edge cases)
  let lastHref = location.href;
  new MutationObserver(() => {
    if (location.href !== lastHref) {
      lastHref = location.href;
      boot();
    }
  }).observe(document, { childList: true, subtree: true });
})();