Ultimate Picture-in-Picture Enhancer

Automatically enable PIP mode with a smooth transition and a configurable, centered control panel.

  1. // ==UserScript==
  2. // @name Ultimate Picture-in-Picture Enhancer
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.2
  5. // @description Automatically enable PIP mode with a smooth transition and a configurable, centered 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. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. // Load saved settings or use defaults
  20. let pipAnimationEnabled = GM_getValue("pipAnimationEnabled", true);
  21. let notificationEnabled = GM_getValue("notificationEnabled", true);
  22. let pipThreshold = GM_getValue("pipThreshold", 0.3);
  23. let pipActive = false;
  24. const iconUrl = "https://images.sftcdn.net/images/t_app-icon-m/p/e858578e-7424-4b99-a13f-c57cd65f8017/4229007087/pip-it-picture-in-picture-logo";
  25.  
  26. // Multi-language support for UI texts
  27. const messages = {
  28. "en": {
  29. "enterPiP": "Page lost focus, video entered Picture-in-Picture mode",
  30. "exitPiP": "Page is back in focus, exiting Picture-in-Picture mode",
  31. "pipSettings": "PIP Enhancer Settings",
  32. "enableAnimation": "Enable Animation",
  33. "disableAnimation": "Disable Animation",
  34. "enableNotifications": "Enable Notifications",
  35. "disableNotifications": "Disable Notifications",
  36. "pipThreshold": "PIP Trigger Ratio"
  37. },
  38. "zh": {
  39. "enterPiP": "网页失去焦点,视频进入画中画模式",
  40. "exitPiP": "网页回到前台,退出画中画模式",
  41. "pipSettings": "画中画增强设置",
  42. "enableAnimation": "启用动画",
  43. "disableAnimation": "禁用动画",
  44. "enableNotifications": "启用通知",
  45. "disableNotifications": "禁用通知",
  46. "pipThreshold": "PIP 触发比例"
  47. },
  48. "es": {
  49. "enterPiP": "La página perdió el foco, el video entró en modo PiP",
  50. "exitPiP": "La página volvió a enfocarse, saliendo del modo PiP",
  51. "pipSettings": "Configuración de PIP Enhancer",
  52. "enableAnimation": "Habilitar animación",
  53. "disableAnimation": "Deshabilitar animación",
  54. "enableNotifications": "Habilitar notificaciones",
  55. "disableNotifications": "Deshabilitar notificaciones",
  56. "pipThreshold": "Proporción de activación de PiP"
  57. }
  58. };
  59.  
  60. // Detect browser language (default to English)
  61. const userLang = navigator.language.startsWith("zh") ? "zh" :
  62. navigator.language.startsWith("es") ? "es" : "en";
  63.  
  64. // Save current settings
  65. function saveSettings() {
  66. GM_setValue("pipAnimationEnabled", pipAnimationEnabled);
  67. GM_setValue("notificationEnabled", notificationEnabled);
  68. GM_setValue("pipThreshold", pipThreshold);
  69. }
  70.  
  71. // Add a single menu command to open the control panel
  72. GM_registerMenuCommand(messages[userLang].pipSettings, openControlPanel);
  73.  
  74. /**
  75. * Checks if a video meets the PIP criteria:
  76. * - Playing
  77. * - Has sound (volume > 0 and not muted)
  78. * - Covers at least pipThreshold of the screen area
  79. */
  80. function isEligibleVideo(video) {
  81. const rect = video.getBoundingClientRect();
  82. const screenArea = window.innerWidth * window.innerHeight;
  83. const videoArea = rect.width * rect.height;
  84. return (
  85. !video.paused &&
  86. video.volume > 0 && !video.muted &&
  87. (videoArea / screenArea) > pipThreshold
  88. );
  89. }
  90.  
  91. /**
  92. * Enters Picture-in-Picture mode.
  93. */
  94. async function enterPiP() {
  95. if (pipActive) return;
  96. const videos = document.querySelectorAll("video");
  97. for (let video of videos) {
  98. if (isEligibleVideo(video)) {
  99. try {
  100. if (pipAnimationEnabled) animatePiP(video);
  101. await video.requestPictureInPicture();
  102. pipActive = true;
  103. if (notificationEnabled) {
  104. GM_notification({
  105. text: messages[userLang].enterPiP,
  106. title: messages[userLang].pipSettings,
  107. timeout: 5000,
  108. image: iconUrl
  109. });
  110. }
  111. } catch (error) {
  112. console.error("Unable to enter PIP mode:", error);
  113. }
  114. break;
  115. }
  116. }
  117. }
  118.  
  119. /**
  120. * Exits Picture-in-Picture mode.
  121. */
  122. function exitPiP() {
  123. if (!pipActive) return;
  124. if (document.pictureInPictureElement) {
  125. document.exitPictureInPicture();
  126. if (notificationEnabled) {
  127. GM_notification({
  128. text: messages[userLang].exitPiP,
  129. title: messages[userLang].pipSettings,
  130. timeout: 5000,
  131. image: iconUrl
  132. });
  133. }
  134. }
  135. pipActive = false;
  136. }
  137.  
  138. /**
  139. * Applies a smooth animation effect to the video element before PIP activation.
  140. */
  141. function animatePiP(video) {
  142. video.style.transition = "transform 0.7s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.7s cubic-bezier(0.4, 0, 0.2, 1)";
  143. video.style.transform = "scale(0.9)";
  144. video.style.opacity = "0.8";
  145. setTimeout(() => {
  146. video.style.transform = "scale(1)";
  147. video.style.opacity = "1";
  148. }, 700);
  149. }
  150.  
  151. /**
  152. * Opens a centered HTML control panel that allows users to configure settings.
  153. */
  154. function openControlPanel() {
  155. // Create panel container
  156. let panel = document.createElement("div");
  157. panel.id = "pip-control-panel";
  158. panel.innerHTML = `
  159. <div class="pip-panel-inner">
  160. <h2>${messages[userLang].pipSettings}</h2>
  161. <div>
  162. <label>
  163. <input type="checkbox" id="pipAnimationCheckbox">
  164. ${messages[userLang].enableAnimation}
  165. </label>
  166. </div>
  167. <div>
  168. <label>
  169. <input type="checkbox" id="pipNotificationsCheckbox">
  170. ${messages[userLang].enableNotifications}
  171. </label>
  172. </div>
  173. <div>
  174. <label>
  175. ${messages[userLang].pipThreshold}:
  176. <input type="number" id="pipThresholdInput" value="${pipThreshold}" step="0.1" min="0" max="1">
  177. </label>
  178. </div>
  179. <button id="pipSaveSettings">Save</button>
  180. <button id="pipClosePanel">Close</button>
  181. </div>
  182. `;
  183. document.body.appendChild(panel);
  184. }
  185.  
  186. // Add some CSS for the control panel using GM_addStyle
  187. GM_addStyle(`
  188. #pip-control-panel {
  189. position: fixed;
  190. top: 50%;
  191. left: 50%;
  192. transform: translate(-50%, -50%);
  193. background: #222;
  194. color: #fff;
  195. padding: 20px;
  196. border-radius: 8px;
  197. z-index: 10000;
  198. box-shadow: 0 4px 12px rgba(0,0,0,0.5);
  199. width: 300px;
  200. font-family: sans-serif;
  201. }
  202. #pip-control-panel .pip-panel-inner {
  203. text-align: center;
  204. }
  205. #pip-control-panel h2 {
  206. margin-top: 0;
  207. font-size: 20px;
  208. }
  209. #pip-control-panel label {
  210. display: block;
  211. margin: 10px 0;
  212. font-size: 14px;
  213. }
  214. #pip-control-panel input[type="number"] {
  215. width: 60px;
  216. margin-left: 5px;
  217. }
  218. #pip-control-panel button {
  219. margin: 10px 5px 0;
  220. padding: 5px 10px;
  221. background: #555;
  222. border: none;
  223. border-radius: 4px;
  224. color: #fff;
  225. cursor: pointer;
  226. }
  227. #pip-control-panel button:hover {
  228. background: #666;
  229. }
  230. `);
  231.  
  232. // Event delegation for the control panel buttons (using event listeners on document)
  233. document.addEventListener("click", function (e) {
  234. if (e.target && e.target.id === "pipSaveSettings") {
  235. // Save the settings from the control panel
  236. pipAnimationEnabled = document.getElementById("pipAnimationCheckbox").checked;
  237. notificationEnabled = document.getElementById("pipNotificationsCheckbox").checked;
  238. pipThreshold = parseFloat(document.getElementById("pipThresholdInput").value);
  239. saveSettings();
  240. document.getElementById("pip-control-panel").remove();
  241. }
  242. if (e.target && e.target.id === "pipClosePanel") {
  243. document.getElementById("pip-control-panel").remove();
  244. }
  245. });
  246.  
  247. // When the control panel is opened, pre-check the current settings.
  248. document.addEventListener("click", function (e) {
  249. if (e.target && e.target.id === "pip-control-panel") {
  250. // do nothing here
  251. }
  252. });
  253.  
  254. // Pre-populate control panel checkboxes when panel is added.
  255. const observer = new MutationObserver((mutationsList, observer) => {
  256. const panel = document.getElementById("pip-control-panel");
  257. if (panel) {
  258. document.getElementById("pipAnimationCheckbox").checked = pipAnimationEnabled;
  259. document.getElementById("pipNotificationsCheckbox").checked = notificationEnabled;
  260. }
  261. });
  262. observer.observe(document.body, { childList: true });
  263.  
  264. /**
  265. * Listen for visibility changes to trigger PIP.
  266. */
  267. document.addEventListener("visibilitychange", function () {
  268. if (document.hidden) {
  269. setTimeout(() => {
  270. if (document.hidden) enterPiP();
  271. }, 300);
  272. } else {
  273. exitPiP();
  274. }
  275. });
  276.  
  277. /**
  278. * Listen for window focus changes.
  279. */
  280. window.addEventListener("blur", enterPiP);
  281. window.addEventListener("focus", exitPiP);
  282.  
  283. })();

QingJ © 2025

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