Greasy Fork is available in English.

Ultimate Picture-in-Picture Enhancer

Automatically enable PIP mode with smooth transition and a control panel

Fra og med 13.02.2025. Se den nyeste version.

  1. // ==UserScript==
  2. // @name Ultimate Picture-in-Picture Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.1
  5. // @description Automatically enable PIP mode with smooth transition and a control panel
  6. // @author OB_BUFF
  7. // @license GPL-3.0
  8. // @match *://*/*
  9. // @grant GM_notification
  10. // @grant GM_addStyle
  11. // @grant GM_registerMenuCommand
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. let pipActive = false;
  18. let pipAnimationEnabled = true;
  19. let notificationEnabled = true;
  20. let pipThreshold = 0.3;
  21. let iconUrl = "https://images.sftcdn.net/images/t_app-icon-m/p/e858578e-7424-4b99-a13f-c57cd65f8017/4229007087/pip-it-picture-in-picture-logo";
  22.  
  23. // Multi-language support
  24. const messages = {
  25. "en": {
  26. "enterPiP": "Page lost focus, video entered Picture-in-Picture mode",
  27. "exitPiP": "Page is back in focus, exiting Picture-in-Picture mode",
  28. "pipSettings": "PIP Enhancer Settings",
  29. "enableAnimation": "Enable Animation",
  30. "disableAnimation": "Disable Animation",
  31. "enableNotifications": "Enable Notifications",
  32. "disableNotifications": "Disable Notifications",
  33. "pipThreshold": "PIP Trigger Ratio"
  34. },
  35. "zh": {
  36. "enterPiP": "网页失去焦点,视频进入画中画模式",
  37. "exitPiP": "网页回到前台,退出画中画模式",
  38. "pipSettings": "画中画增强设置",
  39. "enableAnimation": "启用动画",
  40. "disableAnimation": "禁用动画",
  41. "enableNotifications": "启用通知",
  42. "disableNotifications": "禁用通知",
  43. "pipThreshold": "PIP 触发比例"
  44. },
  45. "es": {
  46. "enterPiP": "La página perdió el foco, el video entró en modo PiP",
  47. "exitPiP": "La página volvió a enfocarse, saliendo del modo PiP",
  48. "pipSettings": "Configuración de PIP Enhancer",
  49. "enableAnimation": "Habilitar animación",
  50. "disableAnimation": "Deshabilitar animación",
  51. "enableNotifications": "Habilitar notificaciones",
  52. "disableNotifications": "Deshabilitar notificaciones",
  53. "pipThreshold": "Proporción de activación de PiP"
  54. }
  55. };
  56.  
  57. // Get browser language
  58. const userLang = navigator.language.startsWith("zh") ? "zh" :
  59. navigator.language.startsWith("es") ? "es" : "en";
  60.  
  61. // Add menu command to open control panel
  62. GM_registerMenuCommand(messages[userLang].pipSettings, () => {
  63. openControlPanel();
  64. });
  65.  
  66. /**
  67. * Checks if a video meets PIP criteria:
  68. * 1. Playing
  69. * 2. Has sound (volume > 0 and not muted)
  70. * 3. Covers at least pipThreshold of screen
  71. */
  72. function isEligibleVideo(video) {
  73. let rect = video.getBoundingClientRect();
  74. let screenWidth = window.innerWidth;
  75. let screenHeight = window.innerHeight;
  76. let videoArea = rect.width * rect.height;
  77. let screenArea = screenWidth * screenHeight;
  78.  
  79. return (
  80. !video.paused &&
  81. video.volume > 0 && !video.muted &&
  82. (videoArea / screenArea) > pipThreshold
  83. );
  84. }
  85.  
  86. /**
  87. * Enters Picture-in-Picture mode
  88. */
  89. async function enterPiP() {
  90. if (pipActive) return;
  91. let videos = document.querySelectorAll("video");
  92. for (let video of videos) {
  93. if (isEligibleVideo(video)) {
  94. try {
  95. if (pipAnimationEnabled) animatePiP(video);
  96. await video.requestPictureInPicture();
  97. pipActive = true;
  98. if (notificationEnabled) {
  99. GM_notification({
  100. text: messages[userLang].enterPiP,
  101. title: messages[userLang].pipSettings,
  102. timeout: 5000,
  103. image: iconUrl
  104. });
  105. }
  106. } catch (error) {
  107. console.error("Unable to enter PIP mode:", error);
  108. }
  109. break;
  110. }
  111. }
  112. }
  113.  
  114. /**
  115. * Exits Picture-in-Picture mode
  116. */
  117. function exitPiP() {
  118. if (!pipActive) return;
  119. if (document.pictureInPictureElement) {
  120. document.exitPictureInPicture();
  121. if (notificationEnabled) {
  122. GM_notification({
  123. text: messages[userLang].exitPiP,
  124. title: messages[userLang].pipSettings,
  125. timeout: 5000,
  126. image: iconUrl
  127. });
  128. }
  129. }
  130. pipActive = false;
  131. }
  132.  
  133. /**
  134. * Smooth PIP animation (scale and fade)
  135. */
  136. function animatePiP(video) {
  137. video.style.transition = "transform 0.5s ease-in-out, opacity 0.5s";
  138. video.style.transform = "scale(0.8)";
  139. video.style.opacity = "0.8";
  140. setTimeout(() => {
  141. video.style.transform = "scale(1)";
  142. video.style.opacity = "1";
  143. }, 500);
  144. }
  145.  
  146. /**
  147. * Open Control Panel (HTML UI)
  148. */
  149. function openControlPanel() {
  150. let panel = document.createElement("div");
  151. panel.innerHTML = `
  152. <div style="position:fixed;bottom:10px;right:10px;padding:10px;background:#222;color:#fff;border-radius:5px;z-index:9999;">
  153. <h3>${messages[userLang].pipSettings}</h3>
  154. <label><input type="checkbox" id="pipAnimation"> ${messages[userLang].enableAnimation}</label><br>
  155. <label><input type="checkbox" id="pipNotifications"> ${messages[userLang].enableNotifications}</label><br>
  156. <label>${messages[userLang].pipThreshold}: <input type="number" id="pipThreshold" value="${pipThreshold}" step="0.1" min="0" max="1"></label><br>
  157. <button id="closePanel">Close</button>
  158. </div>
  159. `;
  160. document.body.appendChild(panel);
  161.  
  162. document.getElementById("pipAnimation").checked = pipAnimationEnabled;
  163. document.getElementById("pipNotifications").checked = notificationEnabled;
  164.  
  165. document.getElementById("pipAnimation").addEventListener("change", (e) => {
  166. pipAnimationEnabled = e.target.checked;
  167. });
  168.  
  169. document.getElementById("pipNotifications").addEventListener("change", (e) => {
  170. notificationEnabled = e.target.checked;
  171. });
  172.  
  173. document.getElementById("pipThreshold").addEventListener("input", (e) => {
  174. pipThreshold = parseFloat(e.target.value);
  175. });
  176.  
  177. document.getElementById("closePanel").addEventListener("click", () => {
  178. panel.remove();
  179. });
  180. }
  181.  
  182. /**
  183. * Listen for visibility change
  184. */
  185. document.addEventListener("visibilitychange", function () {
  186. if (document.hidden) {
  187. setTimeout(() => {
  188. if (document.hidden) enterPiP();
  189. }, 300);
  190. } else {
  191. exitPiP();
  192. }
  193. });
  194.  
  195. /**
  196. * Listen for window focus change
  197. */
  198. window.addEventListener("blur", enterPiP);
  199. window.addEventListener("focus", exitPiP);
  200.  
  201. })();