YouTube Embedded Popupper

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

当前为 2020-06-22 提交的版本,查看 最新版本

  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.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.1.0
  20. Now it doesn't popup when right clicking on the title anchor of the video. You like it?
  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=2',/* 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 'DEFAULT': break;
  134. default: break;
  135. }
  136. return parameters.join(',');
  137. },
  138. readyForPopped: function(){
  139. core.getTargets(site.poppedTargets, RETRY).then(() => {
  140. log("I'm ready for Popped.");
  141. /* pause and play seamlessly */
  142. let originalVideo = site.get.originalVideo(), poppedVideo = elements.video;
  143. if(originalVideo){
  144. poppedVideo.addEventListener('canplay', function(e){
  145. poppedVideo.currentTime = originalVideo.currentTime - REWIND;
  146. originalVideo.pause();
  147. poppedVideo.play();
  148. }, {once: true});
  149. }
  150. /* Enables shortcut keys on popupped window */
  151. poppedVideo.focus();
  152. });
  153. },
  154. getTargets: function(targets, retry = 0){
  155. const get = function(resolve, reject, retry){
  156. for(let i = 0, keys = Object.keys(targets), key; key = keys[i]; i++){
  157. let selected = targets[key]();
  158. if(selected){
  159. if(selected.length) selected.forEach((s) => s.dataset.selector = key);
  160. else selected.dataset.selector = key;
  161. elements[key] = selected;
  162. }else{
  163. if(--retry < 0) return reject(log(`Not found: ${key}, I give up.`));
  164. log(`Not found: ${key}, retrying... (left ${retry})`);
  165. return setTimeout(get, 1000, resolve, reject, retry);
  166. }
  167. }
  168. resolve();
  169. };
  170. return new Promise(function(resolve, reject){
  171. get(resolve, reject, retry);
  172. });
  173. },
  174. addStyle: function(name = 'style'){
  175. if(core.html[name] === undefined) return;
  176. let style = createElement(core.html[name]());
  177. document.head.appendChild(style);
  178. if(elements[name] && elements[name].isConnected) document.head.removeChild(elements[name]);
  179. elements[name] = style;
  180. },
  181. html: {
  182. indicator: () => `
  183. <div id="${SCRIPTID}-indicator"></div>
  184. `,
  185. style: () => `
  186. <style type="text/css">
  187. #${SCRIPTID}-indicator{
  188. position: absolute;
  189. margin: auto;
  190. top: -100%;
  191. bottom: -100%;
  192. left: -100%;
  193. right: -100%;
  194. width: ${sizes.diagonal}px;
  195. height: ${sizes.diagonal}px;
  196. border-radius: ${sizes.diagonal}px;
  197. background: rgba(255,255,255,1.0);
  198. pointer-events: none;
  199. transform: scale(0);
  200. opacity: 1;
  201. transition: 0ms;
  202. }
  203. #${SCRIPTID}-indicator.popped{
  204. transform: scale(1);
  205. opacity: 0;
  206. transition: ${INDICATORDURATION}ms;
  207. }
  208. </style>
  209. `,
  210. },
  211. };
  212. const setTimeout = window.setTimeout, clearTimeout = window.clearTimeout, setInterval = window.setInterval, clearInterval = window.clearInterval, requestAnimationFrame = window.requestAnimationFrame;
  213. const alert = window.alert, confirm = window.confirm, getComputedStyle = window.getComputedStyle, fetch = window.fetch;
  214. if(!('isConnected' in Node.prototype)) Object.defineProperty(Node.prototype, 'isConnected', {get: function(){return document.contains(this)}});
  215. class Storage{
  216. static key(key){
  217. return (SCRIPTID) ? (SCRIPTID + '-' + key) : key;
  218. }
  219. static save(key, value, expire = null){
  220. key = Storage.key(key);
  221. localStorage[key] = JSON.stringify({
  222. value: value,
  223. saved: Date.now(),
  224. expire: expire,
  225. });
  226. }
  227. static read(key){
  228. key = Storage.key(key);
  229. if(localStorage[key] === undefined) return undefined;
  230. let data = JSON.parse(localStorage[key]);
  231. if(data.value === undefined) return data;
  232. if(data.expire === undefined) return data;
  233. if(data.expire === null) return data.value;
  234. if(data.expire < Date.now()) return localStorage.removeItem(key);
  235. return data.value;
  236. }
  237. static delete(key){
  238. key = Storage.key(key);
  239. delete localStorage.removeItem(key);
  240. }
  241. static saved(key){
  242. key = Storage.key(key);
  243. if(localStorage[key] === undefined) return undefined;
  244. let data = JSON.parse(localStorage[key]);
  245. if(data.saved) return data.saved;
  246. else return undefined;
  247. }
  248. }
  249. const $ = function(s, f){
  250. let target = document.querySelector(s);
  251. if(target === null) return null;
  252. return f ? f(target) : target;
  253. };
  254. const $$ = function(s){return document.querySelectorAll(s)};
  255. const animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  256. const createElement = function(html = '<span></span>'){
  257. let outer = document.createElement('div');
  258. outer.innerHTML = html;
  259. return outer.firstElementChild;
  260. };
  261. const log = function(){
  262. if(!DEBUG) return;
  263. let l = log.last = log.now || new Date(), n = log.now = new Date();
  264. let error = new Error(), line = log.format.getLine(error), callers = log.format.getCallers(error);
  265. //console.log(error.stack);
  266. console.log(
  267. (SCRIPTID || '') + ':',
  268. /* 00:00:00.000 */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
  269. /* +0.000s */ '+' + ((n-l)/1000).toFixed(3) + 's',
  270. /* :00 */ ':' + line,
  271. /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
  272. /* caller */ (callers[1] || '') + '()',
  273. ...arguments
  274. );
  275. };
  276. log.formats = [{
  277. name: 'Firefox Scratchpad',
  278. detector: /MARKER@Scratchpad/,
  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 Console',
  283. detector: /MARKER@debugger/,
  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 3',
  288. detector: /\/gm_scripts\//,
  289. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1],
  290. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  291. }, {
  292. name: 'Firefox Greasemonkey 4+',
  293. detector: /MARKER@user-script:/,
  294. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 500,
  295. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  296. }, {
  297. name: 'Firefox Tampermonkey',
  298. detector: /MARKER@moz-extension:/,
  299. getLine: (e) => e.stack.split('\n')[1].match(/([0-9]+):[0-9]+$/)[1] - 6,
  300. getCallers: (e) => e.stack.match(/^[^@]*(?=@)/gm),
  301. }, {
  302. name: 'Chrome Console',
  303. detector: /at MARKER \(<anonymous>/,
  304. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  305. getCallers: (e) => e.stack.match(/[^ ]+(?= \(<anonymous>)/gm),
  306. }, {
  307. name: 'Chrome Tampermonkey',
  308. detector: /at MARKER \(chrome-extension:.*?\/userscript.html\?id=/,
  309. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1] - 6,
  310. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  311. }, {
  312. name: 'Chrome Extension',
  313. detector: /at MARKER \(chrome-extension:/,
  314. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)?$/)[1],
  315. getCallers: (e) => e.stack.match(/[^ ]+(?= \(chrome-extension:)/gm),
  316. }, {
  317. name: 'Edge Console',
  318. detector: /at MARKER \(eval/,
  319. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1],
  320. getCallers: (e) => e.stack.match(/[^ ]+(?= \(eval)/gm),
  321. }, {
  322. name: 'Edge Tampermonkey',
  323. detector: /at MARKER \(Function/,
  324. getLine: (e) => e.stack.split('\n')[2].match(/([0-9]+):[0-9]+\)$/)[1] - 4,
  325. getCallers: (e) => e.stack.match(/[^ ]+(?= \(Function)/gm),
  326. }, {
  327. name: 'Safari',
  328. detector: /^MARKER$/m,
  329. getLine: (e) => 0,/*e.lineが用意されているが最終呼び出し位置のみ*/
  330. getCallers: (e) => e.stack.split('\n'),
  331. }, {
  332. name: 'Default',
  333. detector: /./,
  334. getLine: (e) => 0,
  335. getCallers: (e) => [],
  336. }];
  337. log.format = log.formats.find(function MARKER(f){
  338. if(!f.detector.test(new Error().stack)) return false;
  339. //console.log('////', f.name, 'wants', 0/*line*/, '\n' + new Error().stack);
  340. return true;
  341. });
  342. core.initialize();
  343. if(window === top && console.timeEnd) console.timeEnd(SCRIPTID);
  344. })();

QingJ © 2025

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