您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a floating button to toggle Picture-in-Picture mode for videos on mobile devices.
当前为
// ==UserScript== // @name Floating PIP Button = Enable Picture in Picture for mobile // @name:bg Плаващ PIP бутон = Активиране на картина в картина за мобилни устройства // @name:cs Plovoucí tlačítko PIP = Povolit obraz v obraze pro mobilní zařízení // @name:da Flydende PIP-knap = Aktiver billede i billede til mobile enheder // @name:de Schwebender PIP-Button = Bild-in-Bild für mobile Geräte aktivieren // @name:el Επιπλέων κουμπί PIP = Ενεργοποίηση εικόνας σε εικόνα για κινητές συσκευές // @name:en Floating PIP Button = Enable Picture in Picture for mobile // @name:eo Flosanta PIP-Butono = Ebligi Bildon en Bildo por poŝtelefonoj // @name:es Botón Flotante PIP = Habilita Imagen en Imagen para móvil // @name:es-la Botón Flotante PIP = Habilita Imagen en Imagen para móvil // @name:es-419 Botón Flotante PIP = Habilita Imagen en Imagen para móvil // @name:fi Kelluva PIP-painike = Ota käyttöön kuva kuvassa mobiililaitteille // @name:fr Bouton PIP flottant = Activer l'image dans l'image pour mobile // @name:fr-CA Bouton PIP flottant = Activer l'image dans l'image pour mobile // @name:he כפתור PIP צף = הפעלת תמונה בתוך תמונה לנייד // @name:hr Plutajući PIP gumb = Omogući sliku u slici za mobilne uređaje // @name:hu Lebegő PIP gomb = Kép a képben engedélyezése mobil eszközökre // @name:id Tombol PIP Mengambang = Aktifkan Gambar dalam Gambar untuk seluler // @name:it Pulsante PIP flottante = Abilita immagine nell'immagine per dispositivi mobili // @name:ja 浮動PIPボタン = モバイル用のピクチャーインピクチャーを有効にする // @name:ka მცურავი PIP ღილაკი = ჩართეთ სურათი სურათში მობილური მოწყობილობებისთვის // @name:ko 플로팅 PIP 버튼 = 모바일용 화면 속 화면 활성화 // @name:nb Flytende PIP-knapp = Aktiver bilde i bilde for mobil // @name:nl Zwevende PIP-knop = Schakel beeld in beeld in voor mobiel // @name:pl Pływający przycisk PIP = Włącz obraz w obrazie dla urządzeń mobilnych // @name:pt-BR Botão PIP Flutuante = Ativar imagem em imagem para celular // @name:ro Buton PIP plutitor = Activează imagine în imagine pentru mobil // @name:sv Flytande PIP-knapp = Aktivera bild i bild för mobil // @name:th ปุ่ม PIP ลอย = เปิดใช้งานภาพในภาพสำหรับมือถือ // @name:tr Yüzen PIP Düğmesi = Mobil için Resim içinde Resim'i etkinleştir // @name:ug ھۆلۈپ تۇرغان PIP كۇنۇپكىسى = يانفونلار ئۈچۈن رەسىم ئىچىدە رەسىمنى قوزغىتىش // @name:uk Плаваюча кнопка PIP = Увімкнути картинку в картинці для мобільних пристроїв // @name:vi Nút PIP nổi = Bật chế độ Hình trong Hình cho di động // @name:zh-TW 浮動PIP按鈕 = 啟用行動裝置的畫中畫模式 // @namespace https://jlcareglio.github.io/ // @version 0.9.7 // @description Adds a floating button to toggle Picture-in-Picture mode for videos on mobile devices. // @description:bg Добавя плаващ бутон за превключване на режим картина в картина за видеоклипове на мобилни устройства. // @description:cs Přidává plovoucí tlačítko pro přepínání režimu obraz v obraze pro videa na mobilních zařízeních. // @description:da Tilføjer en flydende knap til at skifte billede-i-billede-tilstand for videoer på mobile enheder. // @description:de Fügt eine schwebende Schaltfläche hinzu, um den Bild-in-Bild-Modus für Videos auf mobilen Geräten umzuschalten. // @description:el Προσθέτει ένα επιπλέον κουμπί για εναλλαγή της λειτουργίας εικόνας σε εικόνα για βίντεο σε κινητές συσκευές. // @description:en Adds a floating button to toggle Picture-in-Picture mode for videos on mobile devices. // @description:eo Aldonas flosantan butonon por ŝalti Bildon en Bildo-reĝimon por videoj en poŝtelefonoj. // @description:es Agrega un botón flotante para alternar el modo Imagen en Imagen para videos en dispositivos móviles. // @description:es-la Agrega un botón flotante para alternar el modo Imagen en Imagen para videos en dispositivos móviles. // @description:es-419 Agrega un botón flotante para alternar el modo Imagen en Imagen para videos en dispositivos móviles. // @description:fi Lisää kelluvan painikkeen, jolla voi vaihtaa kuva kuvassa -tilan videoille mobiililaitteissa. // @description:fr Ajoute un bouton flottant pour basculer en mode image dans l'image pour les vidéos sur les appareils mobiles. // @description:fr-CA Ajoute un bouton flottant pour basculer en mode image dans l'image pour les vidéos sur les appareils mobiles. // @description:he מוסיף כפתור צף למעבר למצב תמונה בתוך תמונה עבור סרטונים במכשירים ניידים. // @description:hr Dodaje plutajući gumb za prebacivanje načina slike u slici za videozapise na mobilnim uređajima. // @description:hu Hozzáad egy lebegő gombot a kép a képben mód váltásához videókhoz mobil eszközökön. // @description:id Menambahkan tombol mengambang untuk beralih ke mode Gambar dalam Gambar untuk video di perangkat seluler. // @description:it Aggiunge un pulsante flottante per attivare la modalità immagine nell'immagine per i video sui dispositivi mobili. // @description:ja モバイルデバイスでビデオのピクチャーインピクチャーモードを切り替えるための浮動ボタンを追加します。 // @description:ka ამატებს მცურავ ღილაკს მობილური მოწყობილობებისთვის ვიდეოების სურათში სურათის რეჟიმის ჩასართავად. // @description:ko 모바일 장치에서 비디오의 화면 속 화면 모드를 전환하는 플로팅 버튼을 추가합니다. // @description:nb Legger til en flytende knapp for å bytte bilde-i-bilde-modus for videoer på mobile enheter. // @description:nl Voegt een zwevende knop toe om de modus Beeld-in-Beeld voor video's op mobiele apparaten in te schakelen. // @description:pl Dodaje pływający przycisk do przełączania trybu obraz w obrazie dla filmów na urządzeniach mobilnych. // @description:pt-BR Adiciona um botão flutuante para alternar o modo Imagem em Imagem para vídeos em dispositivos móveis. // @description:ro Adaugă un buton plutitor pentru a comuta modul imagine în imagine pentru videoclipuri pe dispozitive mobile. // @description:sv Lägger till en flytande knapp för att växla bild-i-bild-läge för videor på mobila enheter. // @description:th เพิ่มปุ่มลอยเพื่อสลับโหมดภาพในภาพสำหรับวิดีโอบนอุปกรณ์เคลื่อนที่ // @description:tr Mobil cihazlarda videolar için Resim içinde Resim modunu değiştirmek için yüzen bir düğme ekler. // @description:ug يانفونلاردا ۋىدىئولار ئۈچۈن رەسىم ئىچىدە رەسىم ھالىتىنى ئالماشتۇرۇش ئۈچۈن ھۆلۈپ تۇرغان كۇنۇپكا قوشىدۇ. // @description:uk Додає плаваючу кнопку для перемикання режиму картинка в картинці для відео на мобільних пристроях. // @description:vi Thêm nút nổi để chuyển đổi chế độ Hình trong Hình cho video trên thiết bị di động. // @description:zh-TW 添加一個浮動按鈕,以切換行動裝置上的影片畫中畫模式。 // @icon https://lh3.googleusercontent.com/cvfpnTKw3B67DtM1ZpJG2PNAIjP6hVMOyYy403X4FMkOuStgG1y4cjCn21vmTnnsip1dTZSVsWBA9IxutGuA3dVDWhg // @grant none // @author Jesús Lautaro Careglio Albornoz // @source https://gist.githubusercontent.com/JLCareglio/22d3f9c9752352a29006f0c90c72d193/raw/01_Floating-PIP-Button.js // @match *://*/* // @license MIT // @compatible firefox // @compatible edge // @compatible kiwi // @supportURL https://gist.github.com/JLCareglio/22d3f9c9752352a29006f0c90c72d193/ // ==/UserScript== (async () => { const CONSTANTS = { BUTTON: { STYLE: ` .pipButton { position: fixed; background-color: rgba(0, 0, 0, 0.5); border-radius: 50%; width: 60px; height: 60px; cursor: pointer; z-index: 9999; display: none; --delete-progress: 0; isolation: isolate; transform: scale(1); transition: transform 0.1s ease-out; } .pipButton:before { pointer-events: none; content: ""; position: absolute; top: 0; bottom: 0; width: 100%; z-index: 2; background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 36' width='100%25' height='100%25'%3E%3Cpath d='M25,17 L17,17 L17,23 L25,23 L25,17 L25,17 Z M29,25 L29,10.98 C29,9.88 28.1,9 27,9 L9,9 C7.9,9 7,9.88 7,10.98 L7,25 C7,26.1 7.9,27 9,27 L27,27 C28.1,27 29,26.1 29,25 L29,25 Z M27,25.02 L9,25.02 L9,10.97 L27,10.97 L27,25.02 L27,25.02 Z' fill='%23fff'/%3E%3C/svg%3E") no-repeat center; } .pipButton:after { content: ""; position: absolute; inset: 0; background-color: rgba(255, 0, 0, 0.8); border-radius: 50%; transform: scale(var(--delete-progress)); transition: transform 0.5s ease; z-index: 1; } `, DEFAULT_POSITION: { right: 20, bottom: 20, }, }, TOUCH: { MOVE_THRESHOLD: 10, CLICK_TIMEOUT: 200, LONG_PRESS_TIMEOUT: 1000, LONG_PRESS_MOVE_THRESHOLD: 15, ANIMATION_DELAY: 300, }, STORAGE: { POSITION_KEY: "pip_button_position", }, }; /** * Main class to handle the PIP button and its functionality */ class PIPButton { #button; #watchedVideos; #observer; #isDragging = false; #touchStartTime = 0; #dragOffset = { x: 0, y: 0 }; #initialPosition = { x: 0, y: 0 }; #longPressTimer = null; #longPressStartPosition = { x: 0, y: 0 }; #animationTimer = null; #isManuallyHidden = false; constructor() { this.#initializeButton(); this.#initializeVideoObserver(); this.#initializeDragHandlers(); this.#detectInitialVideos(); this.#initializeLongPressHandlers(); } /** * Initializes the button and its styles * @private */ #initializeButton() { this.#button = document.createElement("div"); this.#button.classList.add("pipButton"); this.#injectStyles(); document.body.appendChild(this.#button); this.#watchedVideos = new Set(); this.#loadButtonPosition(); } /** * Injects required CSS styles * @private */ #injectStyles() { const style = document.createElement("style"); style.textContent = CONSTANTS.BUTTON.STYLE; document.head.appendChild(style); } /** * Initializes the video observer * @private */ #initializeVideoObserver() { this.#observer = new MutationObserver(this.#handleMutations.bind(this)); this.#observer.observe(document.body, { childList: true, subtree: true, }); } /** * Handles DOM mutations to detect new videos * @private * @param {MutationRecord[]} mutations */ #handleMutations(mutations) { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node instanceof HTMLVideoElement) { this.#addVideo(node); } }); }); this.#updateButtonVisibility(); } /** * Adds a video to the collection of observed videos * @private * @param {HTMLVideoElement} video */ #addVideo(video) { if (!this.#watchedVideos.has(video)) { this.#watchedVideos.add(video); } } /** * Detects existing videos in the DOM on startup * @private */ #detectInitialVideos() { document .querySelectorAll("video") .forEach((video) => this.#addVideo(video)); this.#updateButtonVisibility(); } /** * Toggles PIP mode for the active video * @private */ #togglePIP() { try { if (this.#watchedVideos.size === 0) return; if (document.pictureInPictureElement) { document.exitPictureInPicture(); return; } const playingVideo = Array.from(this.#watchedVideos).find( (video) => !video.paused && !video.ended && video.currentTime > 0 ); const videoToShow = playingVideo || Array.from(this.#watchedVideos)[0]; videoToShow?.requestPictureInPicture().catch(console.error); } catch (error) { console.error("Error toggling PIP:", error); } } /** * Initializes event handlers for dragging * @private */ #initializeDragHandlers() { this.#button.addEventListener( "mousedown", this.#handleDragStart.bind(this) ); this.#button.addEventListener( "touchstart", this.#handleDragStart.bind(this) ); document.addEventListener("mousemove", this.#handleDragMove.bind(this)); document.addEventListener("touchmove", this.#handleDragMove.bind(this), { passive: false, }); document.addEventListener("mouseup", this.#handleDragEnd.bind(this)); document.addEventListener("touchend", this.#handleDragEnd.bind(this)); document.addEventListener("touchcancel", this.#handleDragEnd.bind(this)); } /** * Handles drag start * @private * @param {MouseEvent|TouchEvent} event */ #handleDragStart(event) { this.#isDragging = true; this.#button.style.transform = "scale(2)"; const rect = this.#button.getBoundingClientRect(); this.#initialPosition = { x: rect.left, y: rect.top }; const clientX = event.clientX || event.touches[0].clientX; const clientY = event.clientY || event.touches[0].clientY; this.#dragOffset = { x: clientX - this.#initialPosition.x, y: clientY - this.#initialPosition.y, }; this.#touchStartTime = Date.now(); event.preventDefault(); event.stopPropagation(); if (this.#longPressTimer) { clearTimeout(this.#longPressTimer); } } /** * Handles movement during drag * @private * @param {MouseEvent|TouchEvent} event */ #handleDragMove(event) { if (!this.#isDragging) return; const clientX = event.clientX || event.touches[0].clientX; const clientY = event.clientY || event.touches[0].clientY; const newPosition = this.#calculateNewPosition( clientX - this.#dragOffset.x, clientY - this.#dragOffset.y ); this.#updateButtonPosition(newPosition); event.preventDefault(); event.stopPropagation(); } /** * Calculates new button position * @private * @param {number} x * @param {number} y * @returns {{x: number, y: number}} */ #calculateNewPosition(x, y) { const maxX = window.innerWidth - this.#button.offsetWidth; const maxY = window.innerHeight - this.#button.offsetHeight; return { x: Math.max(0, Math.min(x, maxX)), y: Math.max(0, Math.min(y, maxY)), }; } /** * Updates button position * @private * @param {{x: number, y: number}} position */ #updateButtonPosition(position) { this.#button.style.left = `${position.x}px`; this.#button.style.top = `${position.y}px`; this.#button.style.right = "auto"; this.#button.style.bottom = "auto"; } /** * Handles drag end * @private * @param {MouseEvent|TouchEvent} event */ #handleDragEnd(event) { if (!this.#isDragging) return; this.#button.style.transform = "scale(1)"; const distance = this.#calculateDragDistance(); const elapsedTime = Date.now() - this.#touchStartTime; if ( elapsedTime < CONSTANTS.TOUCH.CLICK_TIMEOUT && distance <= CONSTANTS.TOUCH.MOVE_THRESHOLD && event.button !== 2 ) this.#togglePIP(); const position = { x: this.#button.offsetLeft, y: this.#button.offsetTop, }; if (!this.#isManuallyHidden) localStorage.setItem( CONSTANTS.STORAGE.POSITION_KEY, JSON.stringify(position) ); this.#isDragging = false; event.preventDefault(); event.stopPropagation(); } /** * Calculates drag distance * @private * @returns {number} */ #calculateDragDistance() { const dx = this.#button.offsetLeft - this.#initialPosition.x; const dy = this.#button.offsetTop - this.#initialPosition.y; return Math.sqrt(dx * dx + dy * dy); } /** * Updates button visibility * @private */ #updateButtonVisibility() { this.#button.style.display = this.#watchedVideos.size > 0 && !this.#isManuallyHidden ? "block" : "none"; } /** * Initializes handlers for long press and right-click * @private */ #initializeLongPressHandlers() { this.#button.addEventListener("contextmenu", (e) => { e.preventDefault(); this.#hideButton(); }); const startLongPress = (e) => { const pos = e.touches ? e.touches[0] : e; this.#longPressStartPosition = { x: pos.clientX, y: pos.clientY }; this.#button.style.setProperty("--delete-progress", "0"); this.#animationTimer = setTimeout(() => { requestAnimationFrame(() => { this.#button.style.setProperty("--delete-progress", "1"); }); }, CONSTANTS.TOUCH.ANIMATION_DELAY); this.#longPressTimer = setTimeout(() => { this.#hideButton(); }, CONSTANTS.TOUCH.LONG_PRESS_TIMEOUT); }; const moveDuringPress = (e) => { if (this.#longPressTimer) { const pos = e.touches ? e.touches[0] : e; const moveDistance = Math.sqrt( Math.pow(pos.clientX - this.#longPressStartPosition.x, 2) + Math.pow(pos.clientY - this.#longPressStartPosition.y, 2) ); if (moveDistance > CONSTANTS.TOUCH.LONG_PRESS_MOVE_THRESHOLD) { clearTimeout(this.#longPressTimer); clearTimeout(this.#animationTimer); this.#longPressTimer = null; this.#animationTimer = null; this.#button.style.setProperty("--delete-progress", "0"); } } }; const endLongPress = () => { if (this.#longPressTimer) { clearTimeout(this.#longPressTimer); clearTimeout(this.#animationTimer); this.#button.style.setProperty("--delete-progress", "0"); } }; // Touch events this.#button.addEventListener("touchstart", startLongPress); this.#button.addEventListener("touchmove", moveDuringPress); this.#button.addEventListener("touchend", endLongPress); // Mouse events this.#button.addEventListener("mousedown", (e) => { if (e.button === 0) startLongPress(e); }); this.#button.addEventListener("mousemove", moveDuringPress); this.#button.addEventListener("mouseup", endLongPress); this.#button.addEventListener("mouseleave", endLongPress); } /** * Hides the PIP button * @private */ #hideButton() { this.#isManuallyHidden = true; this.#button.style.display = "none"; } #loadButtonPosition() { const savedPosition = localStorage.getItem( CONSTANTS.STORAGE.POSITION_KEY ); if (savedPosition) { const position = JSON.parse(savedPosition); this.#updateButtonPosition(position); } else { this.#button.style.right = `${CONSTANTS.BUTTON.DEFAULT_POSITION.right}px`; this.#button.style.bottom = `${CONSTANTS.BUTTON.DEFAULT_POSITION.bottom}px`; } } } if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", () => new PIPButton()); else new PIPButton(); })();