NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==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();
})();