Raw Source
XxUnkn0wnxX / YouTube Shorts → Full Player (Action Button + Hotkey)

// ==UserScript==
// @name         YouTube Shorts → Full Player (Action Button + Hotkey)
// @namespace    https://github.com/XxUnkn0wnxX
// @version      2.8.1.3
// @description  Adds a Shorts action-column button and configurable hotkey that open the current YouTube Short in the normal watch player. Vibe coded with OpenAI.
// @homepageURL  https://github.com/XxUnkn0wnxX/Scripts
// @supportURL   https://discord.gg/slayersicerealm
// @author       XxUnkn0wnxX
// @license      AGPL-3.0-or-later
// @updateURL    https://raw.githubusercontent.com/XxUnkn0wnxX/Scripts/master/userscripts/Youtube-shorts-switcher.user.js
// @downloadURL  https://raw.githubusercontent.com/XxUnkn0wnxX/Scripts/master/userscripts/Youtube-shorts-switcher.user.js
// @match        https://www.youtube.com/*
// @match        https://m.youtube.com/*
// @run-at       document-idle
// @grant        none
// @noframes
// ==/UserScript==

(() => {
  'use strict';

  /*** CONFIG ***/
  // Examples: 'W', 'Shift+W', 'Ctrl+Alt+W', 'Space', 'Enter', 'F2', 'ArrowRight'
  const HOTKEY = 'W';

  // ---------- URL / DOM helpers ----------
  function isShortsViewActive() {
    return !!document.querySelector('ytd-reel-player-overlay-renderer, ytd-reel-video-renderer');
  }
  function isWatchViewActive() {
    // IMPORTANT: rely on URL/canonical only; ytd-watch-flexy can exist on Shorts pages too.
    if (location.pathname === '/watch') return true;
    const canon = document.querySelector('link[rel="canonical"]')?.href || '';
    const og = document.querySelector('meta[property="og:url"]')?.content || '';
    return canon.includes('/watch') || og.includes('/watch');
  }
  function isShortsUrl() {
    try {
      if (location.pathname.startsWith('/shorts/')) return true;
      const canon = document.querySelector('link[rel="canonical"]')?.href || '';
      const og = document.querySelector('meta[property="og:url"]')?.content || '';
      return canon.includes('/shorts/') || og.includes('/shorts/');
    } catch { return false; }
  }
  function extractShortsId() {
    const from = (s) => {
      if (!s) return null;
      const i = s.indexOf('/shorts/');
      if (i === -1) return null;
      const rest = s.slice(i + 8);
      const end = rest.search(/[/?#]/);
      return end === -1 ? rest : rest.slice(0, end);
    };
    return from(location.href)
        || from(document.querySelector('link[rel="canonical"]')?.href)
        || from(document.querySelector('meta[property="og:url"]')?.content);
  }
  function buildWatchUrlFromShorts() {
    const id = extractShortsId();
    if (!id) return null;
    const u = new URL('/watch', location.origin);
    u.searchParams.set('v', id);
    return u.toString();
  }
  function redirectToWatch() {
    const dest = buildWatchUrlFromShorts();
    if (dest) location.href = dest;
  }

  // ---------- Hotkey parsing ----------
  const keyNameMap = {
    esc: 'escape', escape: 'escape',
    space: ' ', spacebar: ' ',
    enter: 'enter', return: 'enter',
    backspace: 'backspace', delete: 'delete',
    tab: 'tab',
    arrowleft: 'arrowleft', arrowright: 'arrowright',
    arrowup: 'arrowup', arrowdown: 'arrowdown',
    home: 'home', end: 'end', pageup: 'pageup', pagedown: 'pagedown'
  };
  function parseHotkey(spec) {
    const parts = String(spec).split('+').map(s => s.trim()).filter(Boolean);
    const mods = { shift: false, ctrl: false, alt: false, meta: false };
    let key = '';
    for (const raw of parts) {
      const p = raw.toLowerCase();
      if (p === 'shift') mods.shift = true;
      else if (p === 'ctrl' || p === 'control') mods.ctrl = true;
      else if (p === 'alt' || p === 'option') mods.alt = true;
      else if (p === 'meta' || p === 'cmd' || p === 'command' || p === 'win' || p === 'super') mods.meta = true;
      else key = p;
    }
    if (keyNameMap[key]) key = keyNameMap[key];
    else if (/^f\d{1,2}$/.test(key)) key = key;
    else if (key.length === 1) key = key.toLowerCase();
    else key = key.toLowerCase();
    return { key, ...mods };
  }
  const HOTKEY_CONF = parseHotkey(HOTKEY);

  function matchesHotkey(e, conf) {
    const tag = (e.target && e.target.tagName) ? e.target.tagName.toLowerCase() : '';
    if (tag === 'input' || tag === 'textarea' || (e.target && e.target.isContentEditable)) return false;

    let key = (e.key || '').toLowerCase();
    if (key === ' ') key = ' ';

    return (
      key === conf.key &&
      (!!e.shiftKey === conf.shift) &&
      (!!e.ctrlKey === conf.ctrl) &&
      (!!e.altKey === conf.alt) &&
      (!!e.metaKey === conf.meta)
    );
  }

  // Capture phase so YT/extensions can’t swallow it
  window.addEventListener('keydown', (e) => {
    if (isWatchViewActive()) return;
    if (!(isShortsUrl() || isShortsViewActive())) return;
    if (matchesHotkey(e, HOTKEY_CONF)) {
      e.preventDefault();
      e.stopImmediatePropagation();
      redirectToWatch();
    }
  }, true);

  // ---------- Column button ----------
  let colHost = null;

  function findActionsColumn() {
    const selectors = [
      '.ytReelPlayerOverlayViewModelActionsContainer reel-action-bar-view-model',
      'yt-reel-player-overlay-view-model reel-action-bar-view-model',
      'reel-action-bar-view-model.ytwReelActionBarViewModelHost',
      '.ytReelPlayerOverlayViewModelActionsContainer',
      'ytd-reel-player-overlay-renderer #actions',
      'ytd-reel-video-renderer #actions',
      '#actions.ytd-reel-player-overlay-renderer',
      'ytd-reel-player-overlay-renderer [id="actions"]'
    ];
    for (const s of selectors) {
      const el = document.querySelector(s);
      if (el) return el;
    }
    return null;
  }

  function makeSvgIcon() {
    const svgNS = 'http://www.w3.org/2000/svg';
    const svg = document.createElementNS(svgNS, 'svg');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('width', '24');
    svg.setAttribute('height', '24');
    svg.setAttribute('aria-hidden', 'true');
    const p = document.createElementNS(svgNS, 'path');
    p.setAttribute('d', 'M14 3h7v7h-2V6.41l-5.29 5.3-1.42-1.42 5.3-5.29H14V3zM5 5h7v2H7v10h10v-5h2v7H5V5z');
    svg.appendChild(p);
    return svg;
  }

  function ensureColumnButton() {
    if (!(isShortsUrl() || isShortsViewActive())) return tearDownColumnButton();
    const column = findActionsColumn();
    if (!column) return tearDownColumnButton();
    if (colHost && colHost.isConnected) {
      if (colHost.parentNode === column) return; // already mounted in the active action bar
      colHost.remove();
    }

    colHost = document.createElement('div');
    colHost.className = 'ytwReelActionBarViewModelHostDesktopActionButton';
    const shadow = colHost.attachShadow({ mode: 'open' });

    const style = document.createElement('style');
    style.textContent = `
      :host { display: block; }
      .btnwrap { display: flex; flex-direction: column; align-items: center; gap: 6px; margin: 6px 0; }
      .round { width: 48px; height: 48px; border-radius: 50%; display: grid; place-items: center;
               background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.12);
               color: #fff; cursor: pointer; }
      .round:hover { background: rgba(255,255,255,.14); }
      .label { font: 500 12px/1.1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
               color: #fff; opacity: .9; text-align: center; }
      svg { fill: currentColor; }
    `;

    const wrap = document.createElement('div');
    wrap.className = 'btnwrap';

    const btn = document.createElement('button');
    btn.className = 'round';
    btn.type = 'button';
    btn.title = `Open in Full Player (${HOTKEY})`;
    btn.setAttribute('aria-label', 'Open in Full Player');
    btn.addEventListener('click', redirectToWatch, { capture: true });
    btn.appendChild(makeSvgIcon());

    const lbl = document.createElement('div');
    lbl.className = 'label';
    lbl.textContent = 'Full';

    wrap.append(btn, lbl);
    shadow.append(style, wrap);

    // Put it at the top (above Like)
    column.insertBefore(colHost, column.firstChild);
  }

  function tearDownColumnButton() {
    if (colHost && colHost.isConnected) colHost.remove();
    colHost = null;
  }

  // ---------- Lifecycle ----------
  function refreshMounts() {
    if (isShortsUrl() || isShortsViewActive()) ensureColumnButton();
    else tearDownColumnButton();
  }

  const _ps = history.pushState;
  history.pushState = function (...a) { const r = _ps.apply(this, a); queueMicrotask(refreshMounts); return r; };
  const _rs = history.replaceState;
  history.replaceState = function (...a) { const r = _rs.apply(this, a); queueMicrotask(refreshMounts); return r; };
  window.addEventListener('popstate', () => queueMicrotask(refreshMounts), true);

  let lastHref = location.href;
  setInterval(() => { if (location.href !== lastHref) { lastHref = location.href; refreshMounts(); } }, 400);
  const mo = new MutationObserver(() => refreshMounts());
  mo.observe(document.documentElement, { subtree: true, childList: true });

  refreshMounts();
})();