Greasy Fork is available in English.

YouTube Embedded Popupper

You can pop up embeded videos by right click. (It may require permission for pop up blocker at the first pop)

Pada tanggal 11 Januari 2020. Lihat %(latest_version_link).

// ==UserScript==
// @name        YouTube Embedded Popupper
// @name:ja     YouTube Embedded Popupper
// @name:zh-CN  YouTube Embedded Popupper
// @description You can pop up embeded videos by right click. (It may require permission for pop up blocker at the first pop)
// @description:ja YouTubeの埋め込み動画を、右クリックからポップアップで開けるようにします。(初回のみポップアップブロックの許可が必要かもしれません)
// @description:zh-CN 将YouTube上的嵌入视频从右键弹出打开。(只有第一次,可能需要弹出块的许可)
// @namespace   knoa.jp
// @include     https://www.youtube.com/embed/*
// @include     https://www.youtube-nocookie.com/embed/*
// @version     3.0.0
// @grant       none
// ==/UserScript==

(function(){
  const SCRIPTID = 'YouTubeEmbeddedPopupper';
  const SCRIPTNAME = 'YouTube Embedded Popupper';
  const DEBUG = false;/*
[update] 3.0.0
Now the popped video continues playing from original almost seamlessly.

[bug]

[todo]
本気なら設定パネル

[possible]

[research]
途中まで視聴経験のある動画はstart=0指定時に限り途中からの再生が優先されてしまう

[memo]
  */
  if(window === top && console.time) console.time(SCRIPTID);
  const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  const POPUPWIDTH = 960;/* width of popup window (height depends on the width) */
  const POPUPTOP = 'CENTER';/* position top of popup window (DEFAULT,TOP,CENTER,BOTTOM) */
  const POPUPLEFT = 'CENTER';/* position left of popup window (DEFAULT,LEFT,CENTER,RIGHT) */
  const INDICATORDURATION = 1000*MS;/* duration for indicator animation */
  const REWIND = .0;/* a bit of rewind time for popuping window (seconds) */
  const POPUPTITLE = 'Right Click to Popup';/* shown on mouse hover */
  const PARAMS = [/* overwrite YouTube parameters via https://developers.google.com/youtube/player_parameters */
    'autoplay=1',/* autoplay */
    'controls=2',/* show controls */
    'disablekb=0',/* enable keyboard control */
    'fs=1',/* enable fullscreen */
    'rel=0',/* not to show relative videos */
    'popped=1',/* (original) prevent grandchild popup */
  ];
  const RETRY = 10;
  let site = {
    originalTargets: {
      video: () => $('video'),
    },
    poppedTargets: {
      video: () => $('video'),
    },
    get: {
      originalVideo: () => window.opener.document.querySelector('video'),
    },
  };
  let html, elements = {}, timers = {}, sizes = {};
  let core = {
    initialize: function(){
      html = document.documentElement;
      html.classList.add(SCRIPTID);
      switch(true){
        case(location.href.includes('popped=1')):/* Prevent grandchild popup */
          core.readyForPopped();
          break;
        default:
          core.readyForOriginal();
          break;
      }
    },
    readyForOriginal: function(){
      core.getTargets(site.originalTargets, RETRY).then(() => {
        log("I'm ready for Original.");
        /* Title for Indicator */
        document.body.title = POPUPTITLE;
        /* get window size for pop indicator */
        sizes.innerWidth = document.body.clientWidth;
        sizes.innerHeight = document.body.clientHeight;
        sizes.diagonal = Math.hypot(sizes.innerWidth, sizes.innerHeight);
        /* Right Click to Popup */
        document.body.addEventListener('contextmenu', function(e){
          let video = elements.video;
          elements.indicator.classList.add('popped');
          /* Get current time */
          let params = PARAMS.concat('start=' + parseInt(video.currentTime));
          /* Build URL */
          /* (Duplicated params are overwritten by former) */
          let l = location.href.split('?');
          let url = l[0] + '?' + params.join('&');
          if(l.length === 2) url += ('&' + l[1]);
          /* Open popup window */
          /* (Use URL for window name to prevent popupping the same videos) */
          window.open(url, location.href, core.setOptions());
          e.preventDefault();
          e.stopPropagation();
        }, {capture: true});
        core.createIndicator();
        core.addStyle();
      });
    },
    createIndicator: function(e){
      let indicator = elements.indicator = createElement(core.html.indicator());
      document.body.appendChild(indicator);
      indicator.addEventListener('transitionend', function(e){
        if(indicator.classList.contains('popped')) indicator.classList.remove('popped');
      });
    },
    setOptions: function(){
      let parameters = [], screen = window.screen, body = document.body, width = POPUPWIDTH, height = (width / body.offsetWidth) * body.offsetHeight;
      parameters.push('width=' + width);
      parameters.push('height=' + height);
      switch(POPUPTOP){
        case 'TOP':     parameters.push('top=' + 0); break;
        case 'CENTER':  parameters.push('top=' + (screen.availTop + (screen.availHeight / 2) - (height / 2))); break;
        case 'BOTTOM':  parameters.push('top=' + (screen.availTop + (screen.availHeight) - (height))); break;
        case 'DEFAULT': break;
        default:        break;
      }
      switch(POPUPLEFT){
        case 'LEFT':    parameters.push('left=' + 0); break;
        case 'CENTER':  parameters.push('left=' + (screen.availLeft + (screen.availWidth / 2) - (width / 2))); break;
        case 'RIGHT':   parameters.push('left=' + (screen.availLeft + (screen.availWidth) - (width))); break;
        case 'DEFAULT': break;
        default:        break;
      }
      return parameters.join(',');
    },
    readyForPopped: function(){
      core.getTargets(site.poppedTargets, RETRY).then(() => {
        log("I'm ready for Popped.");
        /* pause and play seamlessly */
        let originalVideo = site.get.originalVideo(), poppedVideo = elements.video;
        if(originalVideo){
          poppedVideo.addEventListener('canplay', function(e){
            poppedVideo.currentTime = originalVideo.currentTime - REWIND;
            originalVideo.pause();
            poppedVideo.play();
          }, {once: true});
        }
        /* Enables shortcut keys on popupped window */
        poppedVideo.focus();
      });
    },
    getTargets: function(targets, retry = 0){
      const get = function(resolve, reject, retry){
        for(let i = 0, keys = Object.keys(targets), key; key = keys[i]; i++){
          let selected = targets[key]();
          if(selected){
            if(selected.length) selected.forEach((s) => s.dataset.selector = key);
            else selected.dataset.selector = key;
            elements[key] = selected;
          }else{
            if(--retry < 0) return reject(log(`Not found: ${key}, I give up.`));
            log(`Not found: ${key}, retrying... (left ${retry})`);
            return setTimeout(get, 1000, resolve, reject, retry);
          }
        }
        resolve();
      };
      return new Promise(function(resolve, reject){
        get(resolve, reject, retry);
      });
    },
    addStyle: function(name = 'style'){
      if(core.html[name] === undefined) return;
      let style = createElement(core.html[name]());
      document.head.appendChild(style);
      if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
      elements[name] = style;
    },
    html: {
      indicator: () => `
        <div id="${SCRIPTID}-indicator"></div>
      `,
      style: () => `
        <style type="text/css">
          #${SCRIPTID}-indicator{
            position: absolute;
            margin: auto;
            top: -100%;
            bottom: -100%;
            left: -100%;
            right: -100%;
            width: ${sizes.diagonal}px;
            height: ${sizes.diagonal}px;
            border-radius: ${sizes.diagonal}px;
            background: rgba(255,255,255,1.0);
            pointer-events: none;
            transform: scale(0);
            opacity: 1;
            transition: 0ms;
          }
          #${SCRIPTID}-indicator.popped{
            transform: scale(1);
            opacity: 0;
            transition: ${INDICATORDURATION}ms;
          }
        </style>
      `,
    },
  };
  const setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, requestAnimationFrame = window.requestAnimationFrame;
  const alert = window.alert, confirm = window.confirm, getComputedStyle = window.getComputedStyle, fetch = window.fetch;
  if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  class Storage{
    static key(key){
      return (SCRIPTID) ? (SCRIPTID + '-' + key) : key;
    }
    static save(key, value, expire = null){
      key = Storage.key(key);
      localStorage[key] = JSON.stringify({
        value: value,
        saved: Date.now(),
        expire: expire,
      });
    }
    static read(key){
      key = Storage.key(key);
      if(localStorage[key] === undefined) return undefined;
      let data = JSON.parse(localStorage[key]);
      if(data.value === undefined) return data;
      if(data.expire === undefined) return data;
      if(data.expire === null) return data.value;
      if(data.expire < Date.now()) return localStorage.removeItem(key);
      return data.value;
    }
    static delete(key){
      key = Storage.key(key);
      delete localStorage.removeItem(key);
    }
    static saved(key){
      key = Storage.key(key);
      if(localStorage[key] === undefined) return undefined;
      let data = JSON.parse(localStorage[key]);
      if(data.saved) return data.saved;
      else return undefined;
    }
  }
  const $ = function(s, f){
    let target = document.querySelector(s);
    if(target === null) return null;
    return f ? f(target) : target;
  };
  const $$ = function(s){return document.querySelectorAll(s)};
  const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  const createElement = function(html = '<span></span>'){
    let outer = document.createElement('div');
    outer.innerHTML = html;
    return outer.firstElementChild;
  };
  const log = function(){
    if(!DEBUG) return;
    let l = log.last = log.now || new Date(), n = log.now = new Date();
    let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
    //console.log(error.stack);
    console.log(
      (SCRIPTID || '') + ':',
      /* 00:00:00.000  */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
      /* +0.000s       */ '+' + ((n-l)/1000).toFixed(3) + 's',
      /* :00           */ ':' + line,
      /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
      /* caller        */ (callers[1] || '') + '()',
      ...arguments
    );
  };
  log.formats = [{
      name: 'Firefox Scratchpad',
      detector: /MARKER@Scratchpad/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Console',
      detector: /MARKER@debugger/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Greasemonkey 3',
      detector: /\/gm_scripts\//,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Greasemonkey 4+',
      detector: /MARKER@user-script:/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Firefox Tampermonkey',
      detector: /MARKER@moz-extension:/,
      getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
      getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
    }, {
      name: 'Chrome Console',
      detector: /at MARKER \(<anonymous>/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
    }, {
      name: 'Chrome Tampermonkey',
      detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 6,
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
    }, {
      name: 'Chrome Extension',
      detector: /at MARKER \(chrome-extension:/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
    }, {
      name: 'Edge Console',
      detector: /at MARKER \(eval/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
    }, {
      name: 'Edge Tampermonkey',
      detector: /at MARKER \(Function/,
      getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
      getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
    }, {
      name: 'Safari',
      detector: /^MARKER$/m,
      getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
      getCallers: (e) => e.stack.split('\n'),
    }, {
      name: 'Default',
      detector: /./,
      getLine: (e) => 0,
      getCallers: (e) => [],
    }];
  log.format = log.formats.find(function MARKER(f){
    if(!f.detector.test(new Error().stack)) return false;
    //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
    return true;
  });
  core.initialize();
  if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
})();