YouTube Embedded Popupper

将YouTube上的嵌入视频从右键弹出打开。(只有第一次,可能需要弹出块的许可)

当前为 2020-01-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Embedded Popupper
  3. // @name:ja YouTube Embedded Popupper
  4. // @name:zh-CN YouTube Embedded Popupper
  5. // @description You can pop up embeded videos by right click. (It may require permission for pop up blocker at the first pop)
  6. // @description:ja YouTubeの埋め込み動画を、右クリックからポップアップで開けるようにします。(初回のみポップアップブロックの許可が必要かもしれません)
  7. // @description:zh-CN 将YouTube上的嵌入视频从右键弹出打开。(只有第一次,可能需要弹出块的许可)
  8. // @namespace knoa.jp
  9. // @include https://www.youtube.com/embed/*
  10. // @include https://www.youtube-nocookie.com/embed/*
  11. // @version 3.0.0
  12. // @grant none
  13. // ==/UserScript==
  14.  
  15. (function(){
  16. const SCRIPTID = 'YouTubeEmbeddedPopupper';
  17. const SCRIPTNAME = 'YouTube Embedded Popupper';
  18. const DEBUG = false;/*
  19. [update] 3.0.0
  20. Now the popped video continues playing from original almost seamlessly.
  21.  
  22. [bug]
  23.  
  24. [todo]
  25. 本気なら設定パネル
  26.  
  27. [possible]
  28.  
  29. [research]
  30. 途中まで視聴経験のある動画はstart=0指定時に限り途中からの再生が優先されてしまう
  31.  
  32. [memo]
  33. */
  34. if(window === top && console.time) console.time(SCRIPTID);
  35. const MS = 1, SECOND = 1000*MS, MINUTE = 60*SECOND, HOUR = 60*MINUTE, DAY = 24*HOUR, WEEK = 7*DAY, MONTH = 30*DAY, YEAR = 365*DAY;
  36. const POPUPWIDTH = 960;/* width of popup window (height depends on the width) */
  37. const POPUPTOP = 'CENTER';/* position top of popup window (DEFAULT,TOP,CENTER,BOTTOM) */
  38. const POPUPLEFT = 'CENTER';/* position left of popup window (DEFAULT,LEFT,CENTER,RIGHT) */
  39. const INDICATORDURATION = 1000*MS;/* duration for indicator animation */
  40. const REWIND = .0;/* a bit of rewind time for popuping window (seconds) */
  41. const POPUPTITLE = 'Right Click to Popup';/* shown on mouse hover */
  42. const PARAMS = [/* overwrite YouTube parameters via https://developers.google.com/youtube/player_parameters */
  43. 'autoplay=1',/* autoplay */
  44. 'controls=2',/* show controls */
  45. 'disablekb=0',/* enable keyboard control */
  46. 'fs=1',/* enable fullscreen */
  47. 'rel=0',/* not to show relative videos */
  48. 'popped=1',/* (original) prevent grandchild popup */
  49. ];
  50. const RETRY = 10;
  51. let site = {
  52. originalTargets: {
  53. video: () => $('video'),
  54. },
  55. poppedTargets: {
  56. video: () => $('video'),
  57. },
  58. get: {
  59. originalVideo: () => window.opener.document.querySelector('video'),
  60. },
  61. };
  62. let html, elements = {}, timers = {}, sizes = {};
  63. let core = {
  64. initialize: function(){
  65. html = document.documentElement;
  66. html.classList.add(SCRIPTID);
  67. switch(true){
  68. case(location.href.includes('popped=1')):/* Prevent grandchild popup */
  69. core.readyForPopped();
  70. break;
  71. default:
  72. core.readyForOriginal();
  73. break;
  74. }
  75. },
  76. readyForOriginal: function(){
  77. core.getTargets(site.originalTargets, RETRY).then(() => {
  78. log("I'm ready for Original.");
  79. /* Title for Indicator */
  80. document.body.title = POPUPTITLE;
  81. /* get window size for pop indicator */
  82. sizes.innerWidth = document.body.clientWidth;
  83. sizes.innerHeight = document.body.clientHeight;
  84. sizes.diagonal = Math.hypot(sizes.innerWidth, sizes.innerHeight);
  85. /* Right Click to Popup */
  86. document.body.addEventListener('contextmenu', function(e){
  87. let video = elements.video;
  88. elements.indicator.classList.add('popped');
  89. /* Get current time */
  90. let params = PARAMS.concat('start=' + parseInt(video.currentTime));
  91. /* Build URL */
  92. /* (Duplicated params are overwritten by former) */
  93. let l = location.href.split('?');
  94. let url = l[0] + '?' + params.join('&');
  95. if(l.length === 2) url += ('&' + l[1]);
  96. /* Open popup window */
  97. /* (Use URL for window name to prevent popupping the same videos) */
  98. window.open(url, location.href, core.setOptions());
  99. e.preventDefault();
  100. e.stopPropagation();
  101. }, {capture: true});
  102. core.createIndicator();
  103. core.addStyle();
  104. });
  105. },
  106. createIndicator: function(e){
  107. let indicator = elements.indicator = createElement(core.html.indicator());
  108. document.body.appendChild(indicator);
  109. indicator.addEventListener('transitionend', function(e){
  110. if(indicator.classList.contains('popped')) indicator.classList.remove('popped');
  111. });
  112. },
  113. setOptions: function(){
  114. let parameters = [], screen = window.screen, body = document.body, width = POPUPWIDTH, height = (width / body.offsetWidth) * body.offsetHeight;
  115. parameters.push('width=' + width);
  116. parameters.push('height=' + height);
  117. switch(POPUPTOP){
  118. case 'TOP': parameters.push('top=' + 0); break;
  119. case 'CENTER': parameters.push('top=' + (screen.availTop + (screen.availHeight / 2) - (height / 2))); break;
  120. case 'BOTTOM': parameters.push('top=' + (screen.availTop + (screen.availHeight) - (height))); break;
  121. case 'DEFAULT': break;
  122. default: break;
  123. }
  124. switch(POPUPLEFT){
  125. case 'LEFT': parameters.push('left=' + 0); break;
  126. case 'CENTER': parameters.push('left=' + (screen.availLeft + (screen.availWidth / 2) - (width / 2))); break;
  127. case 'RIGHT': parameters.push('left=' + (screen.availLeft + (screen.availWidth) - (width))); break;
  128. case 'DEFAULT': break;
  129. default: break;
  130. }
  131. return parameters.join(',');
  132. },
  133. readyForPopped: function(){
  134. core.getTargets(site.poppedTargets, RETRY).then(() => {
  135. log("I'm ready for Popped.");
  136. /* pause and play seamlessly */
  137. let originalVideo = site.get.originalVideo(), poppedVideo = elements.video;
  138. if(originalVideo){
  139. poppedVideo.addEventListener('canplay', function(e){
  140. poppedVideo.currentTime = originalVideo.currentTime - REWIND;
  141. originalVideo.pause();
  142. poppedVideo.play();
  143. }, {once: true});
  144. }
  145. /* Enables shortcut keys on popupped window */
  146. poppedVideo.focus();
  147. });
  148. },
  149. getTargets: function(targets, retry = 0){
  150. const get = function(resolve, reject, retry){
  151. for(let i = 0, keys = Object.keys(targets), key; key = keys[i]; i++){
  152. let selected = targets[key]();
  153. if(selected){
  154. if(selected.length) selected.forEach((s) => s.dataset.selector = key);
  155. else selected.dataset.selector = key;
  156. elements[key] = selected;
  157. }else{
  158. if(--retry < 0) return reject(log(`Not found: ${key}, I give up.`));
  159. log(`Not found: ${key}, retrying... (left ${retry})`);
  160. return setTimeout(get, 1000, resolve, reject, retry);
  161. }
  162. }
  163. resolve();
  164. };
  165. return new Promise(function(resolve, reject){
  166. get(resolve, reject, retry);
  167. });
  168. },
  169. addStyle: function(name = 'style'){
  170. if(core.html[name] === undefined) return;
  171. let style = createElement(core.html[name]());
  172. document.head.appendChild(style);
  173. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  174. elements[name] = style;
  175. },
  176. html: {
  177. indicator: () => `
  178. <div id="${SCRIPTID}-indicator"></div>
  179. `,
  180. style: () => `
  181. <style type="text/css">
  182. #${SCRIPTID}-indicator{
  183. position: absolute;
  184. margin: auto;
  185. top: -100%;
  186. bottom: -100%;
  187. left: -100%;
  188. right: -100%;
  189. width: ${sizes.diagonal}px;
  190. height: ${sizes.diagonal}px;
  191. border-radius: ${sizes.diagonal}px;
  192. background: rgba(255,255,255,1.0);
  193. pointer-events: none;
  194. transform: scale(0);
  195. opacity: 1;
  196. transition: 0ms;
  197. }
  198. #${SCRIPTID}-indicator.popped{
  199. transform: scale(1);
  200. opacity: 0;
  201. transition: ${INDICATORDURATION}ms;
  202. }
  203. </style>
  204. `,
  205. },
  206. };
  207. const setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, requestAnimationFrame = window.requestAnimationFrame;
  208. const alert = window.alert, confirm = window.confirm, getComputedStyle = window.getComputedStyle, fetch = window.fetch;
  209. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  210. class Storage{
  211. static key(key){
  212. return (SCRIPTID) ? (SCRIPTID + '-' + key) : key;
  213. }
  214. static save(key, value, expire = null){
  215. key = Storage.key(key);
  216. localStorage[key] = JSON.stringify({
  217. value: value,
  218. saved: Date.now(),
  219. expire: expire,
  220. });
  221. }
  222. static read(key){
  223. key = Storage.key(key);
  224. if(localStorage[key] === undefined) return undefined;
  225. let data = JSON.parse(localStorage[key]);
  226. if(data.value === undefined) return data;
  227. if(data.expire === undefined) return data;
  228. if(data.expire === null) return data.value;
  229. if(data.expire < Date.now()) return localStorage.removeItem(key);
  230. return data.value;
  231. }
  232. static delete(key){
  233. key = Storage.key(key);
  234. delete localStorage.removeItem(key);
  235. }
  236. static saved(key){
  237. key = Storage.key(key);
  238. if(localStorage[key] === undefined) return undefined;
  239. let data = JSON.parse(localStorage[key]);
  240. if(data.saved) return data.saved;
  241. else return undefined;
  242. }
  243. }
  244. const $ = function(s, f){
  245. let target = document.querySelector(s);
  246. if(target === null) return null;
  247. return f ? f(target) : target;
  248. };
  249. const $$ = function(s){return document.querySelectorAll(s)};
  250. const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  251. const createElement = function(html = '<span></span>'){
  252. let outer = document.createElement('div');
  253. outer.innerHTML = html;
  254. return outer.firstElementChild;
  255. };
  256. const log = function(){
  257. if(!DEBUG) return;
  258. let l = log.last = log.now || new Date(), n = log.now = new Date();
  259. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  260. //console.log(error.stack);
  261. console.log(
  262. (SCRIPTID || '') + ':',
  263. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  264. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  265. /* :00 */ ':' + line,
  266. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  267. /* caller */ (callers[1] || '') + '()',
  268. ...arguments
  269. );
  270. };
  271. log.formats = [{
  272. name: 'Firefox Scratchpad',
  273. detector: /MARKER@Scratchpad/,
  274. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  275. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  276. }, {
  277. name: 'Firefox Console',
  278. detector: /MARKER@debugger/,
  279. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  280. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  281. }, {
  282. name: 'Firefox Greasemonkey 3',
  283. detector: /\/gm_scripts\//,
  284. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  285. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  286. }, {
  287. name: 'Firefox Greasemonkey 4+',
  288. detector: /MARKER@user-script:/,
  289. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  290. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  291. }, {
  292. name: 'Firefox Tampermonkey',
  293. detector: /MARKER@moz-extension:/,
  294. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  295. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  296. }, {
  297. name: 'Chrome Console',
  298. detector: /at MARKER \(<anonymous>/,
  299. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  300. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  301. }, {
  302. name: 'Chrome Tampermonkey',
  303. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  304. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 6,
  305. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  306. }, {
  307. name: 'Chrome Extension',
  308. detector: /at MARKER \(chrome-extension:/,
  309. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  310. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  311. }, {
  312. name: 'Edge Console',
  313. detector: /at MARKER \(eval/,
  314. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  315. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  316. }, {
  317. name: 'Edge Tampermonkey',
  318. detector: /at MARKER \(Function/,
  319. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  320. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  321. }, {
  322. name: 'Safari',
  323. detector: /^MARKER$/m,
  324. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  325. getCallers: (e) => e.stack.split('\n'),
  326. }, {
  327. name: 'Default',
  328. detector: /./,
  329. getLine: (e) => 0,
  330. getCallers: (e) => [],
  331. }];
  332. log.format = log.formats.find(function MARKER(f){
  333. if(!f.detector.test(new Error().stack)) return false;
  334. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  335. return true;
  336. });
  337. core.initialize();
  338. if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
  339. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址