Easy Picture-in-Picture

Picture in Picture を簡単に利用できるようにポップアウト ボタンを追加します。

当前为 2019-09-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Easy Picture-in-Picture
  3. // @namespace easy-picture-in-picture.user.js
  4. // @version 1.9
  5. // @description Picture in Picture を簡単に利用できるようにポップアウト ボタンを追加します。
  6. // @author nafumofu
  7. // @match *://*/*
  8. // @grant GM_addStyle
  9. // @grant GM_getResourceText
  10. // @license MIT License
  11. // ==/UserScript==
  12.  
  13. class EasyPictureInPicture {
  14. constructor() {
  15. if (document.pictureInPictureEnabled) {
  16. document.body.addEventListener('mousemove', (evt) => this.event(evt), {passive: true});
  17. document.body.addEventListener('touchstart', (evt) => this.event(evt), {passive: true});
  18. }
  19. }
  20. event(evt) {
  21. if (!this.eventLocked) {
  22. this.eventLocked = !!setTimeout(() => {
  23. this.eventLocked = false;
  24. }, 50);
  25. var posX = evt.clientX || evt.changedTouches[0].clientX;
  26. var posY = evt.clientY || evt.changedTouches[0].clientY;
  27. var elems = document.elementsFromPoint(posX, posY);
  28. for (let elem of elems) {
  29. if (elem.tagName === 'VIDEO' && elem.readyState) {
  30. this.showButton(elem);
  31. break;
  32. }
  33. }
  34. }
  35. }
  36. popOut() {
  37. if (document.pictureInPictureElement === this.epipTarget) {
  38. document.exitPictureInPicture();
  39. return
  40. }
  41. this.epipTarget.requestPictureInPicture();
  42. }
  43. showButton(target) {
  44. if (!this.epipButton) {
  45. this.epipButton = this.createButton();
  46. }
  47. if (!target.disablePictureInPicture) {
  48. this.epipTarget = target;
  49. var style = this.epipButton.style;
  50. var compStyle = getComputedStyle(this.epipButton);
  51. var rect =this.epipTarget.getBoundingClientRect();
  52. var posY = window.scrollY + rect.top;
  53. var posX = window.scrollX + rect.left + (rect.width / 2 - parseInt(compStyle.width) / 2);
  54. style.setProperty('top', `${posY}px`, 'important');
  55. style.setProperty('left', `${posX}px`, 'important');
  56. style.setProperty('opacity', '1', 'important');
  57. style.setProperty('pointer-events', 'auto', 'important');
  58. clearTimeout(this.epipTimer);
  59. this.epipTimer = setTimeout(() => {
  60. style.setProperty('opacity', '0', 'important');
  61. style.setProperty('pointer-events', 'none', 'important');
  62. }, 3000);
  63. }
  64. }
  65. createButton() {
  66. // https://material.io/resources/icons/?icon=picture_in_picture_alt&style=round
  67. var resIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M18 11h-6c-.55 0-1 .45-1 1v4c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-4c0-.55-.45-1-1-1zm5 8V4.98C23 3.88 22.1 3 21 3H3c-1.1 0-2 .88-2 1.98V19c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-3 .02H4c-.55 0-1-.45-1-1V5.97c0-.55.45-1 1-1h16c.55 0 1 .45 1 1v12.05c0 .55-.45 1-1 1z"/></svg>`;
  68. GM_addStyle(`
  69. #epip-button {
  70. all: unset !important;
  71. z-index: 2147483647 !important;
  72. position: absolute !important;
  73. pointer-events: none !important;
  74. opacity: 0 !important;
  75. transition: opacity 0.3s !important;
  76. margin-top: 4px !important;
  77. }
  78. #epip-button > .epip-icon {
  79. all: unset !important;
  80. fill: rgba(255,255,255,0.95) !important;
  81. background: rgba(0,0,0,0.5) !important;
  82. width: 20px !important;
  83. height: 20px !important;
  84. padding: 6px !important;
  85. border-radius: 50% !important;
  86. }
  87. `);
  88. var button = document.createElement('button');
  89. button.id = 'epip-button';
  90. button.tabIndex = -1;
  91. button.addEventListener('click', () => this.popOut());
  92. button.insertAdjacentHTML('afterbegin', resIcon);
  93. button.firstChild.classList.add('epip-icon');
  94. document.documentElement.append(button);
  95. return button;
  96. }
  97. }
  98.  
  99. new EasyPictureInPicture();