Ultimate Picture-in-Picture Enhancer

Automatically enable PIP mode with smooth transition and control panel

目前为 2025-02-13 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Ultimate Picture-in-Picture Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.0
  5. // @description Automatically enable PIP mode with smooth transition and 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 (Current: {value})"
  34. },
  35. "zh": {
  36. "enterPiP": "网页失去焦点,视频进入画中画模式",
  37. "exitPiP": "网页回到前台,退出画中画模式",
  38. "pipSettings": "画中画增强设置",
  39. "enableAnimation": "启用动画",
  40. "disableAnimation": "禁用动画",
  41. "enableNotifications": "启用通知",
  42. "disableNotifications": "禁用通知",
  43. "pipThreshold": "PIP 触发比例 (当前: {value})"
  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 (Actual: {value})"
  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 Tampermonkey menu options
  62. GM_registerMenuCommand(messages[userLang].enableAnimation, () => {
  63. pipAnimationEnabled = true;
  64. });
  65.  
  66. GM_registerMenuCommand(messages[userLang].disableAnimation, () => {
  67. pipAnimationEnabled = false;
  68. });
  69.  
  70. GM_registerMenuCommand(messages[userLang].enableNotifications, () => {
  71. notificationEnabled = true;
  72. });
  73.  
  74. GM_registerMenuCommand(messages[userLang].disableNotifications, () => {
  75. notificationEnabled = false;
  76. });
  77.  
  78. GM_registerMenuCommand(messages[userLang].pipThreshold.replace("{value}", pipThreshold), () => {
  79. let newThreshold = prompt(messages[userLang].pipThreshold.replace("{value}", pipThreshold), pipThreshold);
  80. if (newThreshold !== null) {
  81. pipThreshold = parseFloat(newThreshold);
  82. }
  83. });
  84.  
  85. /**
  86. * Checks if a video meets PIP criteria:
  87. * 1. Playing
  88. * 2. Has sound (volume > 0 and not muted)
  89. * 3. Covers at least pipThreshold of screen
  90. */
  91. function isEligibleVideo(video) {
  92. let rect = video.getBoundingClientRect();
  93. let screenWidth = window.innerWidth;
  94. let screenHeight = window.innerHeight;
  95. let videoArea = rect.width * rect.height;
  96. let screenArea = screenWidth * screenHeight;
  97.  
  98. return (
  99. !video.paused &&
  100. video.volume > 0 && !video.muted &&
  101. (videoArea / screenArea) > pipThreshold
  102. );
  103. }
  104.  
  105. /**
  106. * Enters Picture-in-Picture mode
  107. */
  108. async function enterPiP() {
  109. if (pipActive) return;
  110. let videos = document.querySelectorAll("video");
  111. for (let video of videos) {
  112. if (isEligibleVideo(video)) {
  113. try {
  114. if (pipAnimationEnabled) animatePiP(video);
  115. await video.requestPictureInPicture();
  116. pipActive = true;
  117. if (notificationEnabled) {
  118. GM_notification({
  119. text: messages[userLang].enterPiP,
  120. title: messages[userLang].pipSettings,
  121. timeout: 5000,
  122. image: iconUrl
  123. });
  124. }
  125. } catch (error) {
  126. console.error("Unable to enter PIP mode:", error);
  127. }
  128. break;
  129. }
  130. }
  131. }
  132.  
  133. /**
  134. * Exits Picture-in-Picture mode
  135. */
  136. function exitPiP() {
  137. if (!pipActive) return;
  138. if (document.pictureInPictureElement) {
  139. document.exitPictureInPicture();
  140. if (notificationEnabled) {
  141. GM_notification({
  142. text: messages[userLang].exitPiP,
  143. title: messages[userLang].pipSettings,
  144. timeout: 5000,
  145. image: iconUrl
  146. });
  147. }
  148. }
  149. pipActive = false;
  150. }
  151.  
  152. /**
  153. * Smooth PIP animation (scale and fade)
  154. */
  155. function animatePiP(video) {
  156. video.style.transition = "transform 0.5s ease-in-out, opacity 0.5s";
  157. video.style.transform = "scale(0.8)";
  158. video.style.opacity = "0.8";
  159. setTimeout(() => {
  160. video.style.transform = "scale(1)";
  161. video.style.opacity = "1";
  162. }, 500);
  163. }
  164.  
  165. /**
  166. * Listen for visibility change
  167. */
  168. document.addEventListener("visibilitychange", function () {
  169. if (document.hidden) {
  170. setTimeout(() => {
  171. if (document.hidden) enterPiP();
  172. }, 300);
  173. } else {
  174. exitPiP();
  175. }
  176. });
  177.  
  178. /**
  179. * Listen for window focus change
  180. */
  181. window.addEventListener("blur", enterPiP);
  182. window.addEventListener("focus", exitPiP);
  183.  
  184. })();