YouTube Embedded Popupper

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

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

QingJ © 2025

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