Skip to content

Instantly share code, notes, and snippets.

@JLCareglio
Last active February 8, 2025 21:11
Show Gist options
  • Save JLCareglio/22d3f9c9752352a29006f0c90c72d193 to your computer and use it in GitHub Desktop.
Save JLCareglio/22d3f9c9752352a29006f0c90c72d193 to your computer and use it in GitHub Desktop.
🇺🇸 Adds a floating button to toggle Picture-in-Picture mode for videos on mobile devices. 🇦🇷 Agrega un botón flotante para alternar el modo Imagen en Imagen para videos en dispositivos móviles.

Floating PIP Button

This UserScript adds a floating button that enables Picture-in-Picture (PIP) mode for videos on mobile devices.

imageimageimage

License: MIT Tampermonkey YouTube

Firefox Edge

✨ Features

  • Adds a draggable floating button to enable PIP mode
  • Compatible with any website that has videos
  • Button appears automatically when videos are detected
  • Easy to use: one tap to toggle PIP
  • Long press or right-click to hide the button

🚀 Installation

  1. Install Tampermonkey or another userscript manager
  2. Click here to install the script
  3. Visit any site with videos to try it out

💡 Usage

  1. Navigate to any website containing videos
  2. You'll see a floating PIP button when a video is detected
  3. Tap the button to toggle PIP mode
  4. Drag the button to reposition it
  5. Long press to hide the button
// ==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 1.1.0
// @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.user.js
// @match *://*/*
// @updateURL https://gist.githubusercontent.com/JLCareglio/22d3f9c9752352a29006f0c90c72d193/raw/01_Floating-PIP-Button.user.js
// @downloadURL https://gist.githubusercontent.com/JLCareglio/22d3f9c9752352a29006f0c90c72d193/raw/01_Floating-PIP-Button.user.js
// @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",
},
};
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();
}
initializeButton() {
this.button = document.createElement("div");
this.button.classList.add("pipButton");
this.injectStyles();
document.body.appendChild(this.button);
this.watchedVideos = new Set();
this.loadButtonPosition();
this.checkButtonPosition();
window.addEventListener("resize", () => this.checkButtonPosition());
}
injectStyles() {
const style = document.createElement("style");
style.textContent = CONSTANTS.BUTTON.STYLE;
document.head.appendChild(style);
}
initializeVideoObserver() {
this.observer = new MutationObserver(this.handleMutations.bind(this));
this.observer.observe(document.body, {
childList: true,
subtree: true,
});
}
handleMutations(mutations) {
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLVideoElement) {
this.addVideo(node);
}
});
});
this.updateButtonVisibility();
}
addVideo(video) {
if (!this.watchedVideos.has(video)) {
this.watchedVideos.add(video);
}
}
detectInitialVideos() {
document
.querySelectorAll("video")
.forEach((video) => this.addVideo(video));
this.updateButtonVisibility();
}
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()
.then(() => {
Object.defineProperty(document, "visibilityState", {
get: () => "visible",
});
})
.catch(console.error);
} catch (error) {
console.error("Error toggling PIP:", error);
}
}
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));
}
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);
}
}
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();
}
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)),
};
}
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";
}
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();
}
calculateDragDistance() {
const dx = this.button.offsetLeft - this.initialPosition.x;
const dy = this.button.offsetTop - this.initialPosition.y;
return Math.sqrt(dx * dx + dy * dy);
}
updateButtonVisibility() {
this.button.style.display =
this.watchedVideos.size > 0 && !this.isManuallyHidden
? "block"
: "none";
}
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);
}
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`;
}
}
checkButtonPosition() {
const rect = this.button.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const newLeft = Math.max(
0,
Math.min(rect.left, windowWidth - rect.width)
);
const newTop = Math.max(
0,
Math.min(rect.top, windowHeight - rect.height)
);
if (newLeft !== rect.left || newTop !== rect.top)
this.updateButtonPosition({ x: newLeft, y: newTop });
}
}
if (document.readyState === "loading")
document.addEventListener("DOMContentLoaded", () => new PIPButton());
else new PIPButton();
})();

Floating PIP Button

This UserScript adds a floating button that enables Picture-in-Picture (PIP) mode for videos on mobile devices.

imageimageimage

License: MIT Tampermonkey

Firefox Edge

✨ Features

  • Adds a draggable floating button to enable PIP mode
  • Compatible with any website that has videos
  • Button appears automatically when videos are detected
  • Easy to use: one tap to toggle PIP
  • Long press or right-click to hide the button

🚀 Installation

  1. Install Tampermonkey or another userscript manager
  2. Click here to install the script
  3. Visit any site with videos to try it out

💡 Usage

  1. Navigate to any website containing videos
  2. You'll see a floating PIP button when a video is detected
  3. Tap the button to toggle PIP mode
  4. Drag the button to reposition it
  5. Long press to hide the button

Botón Flotante PIP

Este UserScript agrega un botón flotante que permite activar el modo Picture-in-Picture (PIP) para videos en dispositivos móviles.

imageimageimage

License: MIT Tampermonkey

Firefox Edge

✨ Características

  • Agrega un botón flotante y arrastrable para activar el modo PIP
  • Compatible con cualquier sitio web que tenga videos
  • El botón aparece automáticamente cuando detecta videos
  • Fácil de usar: un toque para activar/desactivar PIP
  • Mantén presionado o haz clic derecho para ocultar el botón

🚀 Instalación

  1. Instala Tampermonkey u otro gestor de userscripts
  2. Haz clic aquí para instalar el script
  3. Visita cualquier sitio con videos para probarlo

💡 Uso

  1. Navega a cualquier sitio web que contenga videos
  2. Verás un botón PIP flotante cuando se detecte un video
  3. Toca el botón para activar/desactivar el modo PIP
  4. Arrastra el botón para reposicionarlo
  5. Mantén presionado para ocultar el botón

Pulsante PIP Flottante

Questo UserScript aggiunge un pulsante flottante che attiva la modalità Picture-in-Picture (PIP) per i video su dispositivi mobili.

imageimageimage

License: MIT Tampermonkey

Firefox Edge

✨ Caratteristiche

  • Aggiunge un pulsante flottante trascinabile per attivare la modalità PIP
  • Compatibile con qualsiasi sito web che contiene video
  • Il pulsante appare automaticamente quando vengono rilevati i video
  • Facile da usare: un tocco per attivare/disattivare il PIP
  • Pressione lunga o clic destro per nascondere il pulsante

🚀 Installazione

  1. Installa Tampermonkey o un altro gestore di userscript
  2. Clicca qui per installare lo script
  3. Visita qualsiasi sito con video per provarlo

💡 Utilizzo

  1. Naviga su qualsiasi sito web contenente video
  2. Vedrai un pulsante PIP flottante quando viene rilevato un video
  3. Tocca il pulsante per attivare/disattivare la modalità PIP
  4. Trascina il pulsante per riposizionarlo
  5. Pressione lunga per nascondere il pulsante

フローティングPIPボタン

このユーザースクリプトは、モバイルデバイスでのビデオのピクチャーインピクチャー(PIP)モードを有効にするフローティングボタンを追加します。

imageimageimage

License: MIT Tampermonkey

Firefox Edge

✨ 機能

  • PIPモードを有効にするドラッグ可能なフローティングボタンを追加
  • 動画のあるすべてのウェブサイトで使用可能
  • 動画が検出されると自動的にボタンが表示
  • 使いやすい:タップ一つでPIPモードの切り替え
  • 長押しまたは右クリックでボタンを非表示

🚀 インストール

  1. Tampermonkeyまたは他のユーザースクリプトマネージャーをインストール
  2. ここをクリックしてスクリプトをインストール
  3. 動画のあるサイトにアクセスして試してみましょう

💡 使用方法

  1. 動画のあるウェブサイトにアクセス
  2. 動画が検出されるとフローティングPIPボタンが表示されます
  3. ボタンをタップしてPIPモードを切り替え
  4. ボタンをドラッグして位置を変更
  5. 長押しでボタンを非表示
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment