Greasy Fork is available in English.
Webcam capture
// ==UserScript==
// @name Gemini Webcam Capture
// @namespace http://tampermonkey.net/
// @version 2.4
// @description Webcam capture
// @author You
// @match https://gemini.google.com/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// URL chat saat foto diambil — null = belum ada capture
let capturedOnUrl = null;
// Ambil URL chat aktif saat ini (path saja, tanpa query/hash)
const getCurrentChatUrl = () => location.pathname;
// Cek apakah kita masih di chat yang sama saat foto diambil
const isOnSameChat = () => {
if (!capturedOnUrl) return false;
return getCurrentChatUrl() === capturedOnUrl;
};
// Monitor navigasi SPA Gemini (URL berubah tanpa full reload)
let lastUrl = getCurrentChatUrl();
setInterval(() => {
const currentUrl = getCurrentChatUrl();
if (currentUrl !== lastUrl) {
console.log(`[Webcam] Navigasi terdeteksi: ${lastUrl} → ${currentUrl}`);
lastUrl = currentUrl;
// Jangan reset capturedOnUrl — biarkan guard isOnSameChat() yang bekerja
}
}, 300);
// ── UI ──────────────────────────────────────────────────────────────────
const triggerBtn = document.createElement('button');
triggerBtn.textContent = '📷 Webcam';
triggerBtn.style.cssText = 'font-weight:bold; position:fixed; bottom:24px; left:24px; z-index:9999; padding:10px 16px; border-radius:24px; border:none; background-color:#1a73e8; color:#fff; cursor:pointer; box-shadow:0 2px 6px rgba(0,0,0,0.3);';
document.body.appendChild(triggerBtn);
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.8); z-index:10000; display:none; flex-direction:column; justify-content:center; align-items:center;';
const videoElement = document.createElement('video');
videoElement.style.cssText = 'width:640px; max-width:90%; border-radius:12px; background:#000; transform:scaleX(-1);';
videoElement.autoplay = true;
const canvasElement = document.createElement('canvas');
canvasElement.style.display = 'none';
const statusText = document.createElement('div');
statusText.style.cssText = 'margin-top:12px; color:#fff; font-size:14px; min-height:20px;';
const controlsDiv = document.createElement('div');
controlsDiv.style.cssText = 'margin-top:20px; display:flex; gap:15px;';
const captureBtn = document.createElement('button');
captureBtn.textContent = '⏱️ Ambil (3 Detik)';
captureBtn.style.cssText = 'padding:12px 24px; font-size:16px; cursor:pointer; border-radius:8px; border:none; background-color:#34a853; color:#fff;';
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Tutup';
closeBtn.style.cssText = 'padding:12px 24px; font-size:16px; cursor:pointer; border-radius:8px; border:none; background-color:#ea4335; color:#fff;';
controlsDiv.appendChild(captureBtn);
controlsDiv.appendChild(closeBtn);
overlay.appendChild(videoElement);
overlay.appendChild(canvasElement);
overlay.appendChild(controlsDiv);
overlay.appendChild(statusText);
document.body.appendChild(overlay);
let mediaStream = null;
// ── Kamera ──────────────────────────────────────────────────────────────
triggerBtn.addEventListener('click', async () => {
overlay.style.display = 'flex';
statusText.textContent = '';
// Reset: foto baru = sesi baru
capturedOnUrl = null;
try {
mediaStream = await navigator.mediaDevices.getUserMedia({ video: true });
videoElement.srcObject = mediaStream;
} catch (err) {
alert('Kamera Error: ' + err.message);
closeCamera();
}
});
const closeCamera = () => {
overlay.style.display = 'none';
if (mediaStream) {
mediaStream.getTracks().forEach(track => track.stop());
mediaStream = null;
}
videoElement.srcObject = null;
statusText.textContent = '';
};
closeBtn.addEventListener('click', closeCamera);
// Bersihkan clipboard sistem
const clearClipboard = async () => {
try {
await navigator.clipboard.write([
new ClipboardItem({ 'text/plain': new Blob([''], { type: 'text/plain' }) })
]);
console.log('[Webcam] Clipboard dibersihkan');
} catch (e) {
console.warn('[Webcam] Gagal bersihkan clipboard:', e);
}
};
// ── Paste ───────────────────────────────────────────────────────────────
const autoPasteToGemini = async (blob) => {
// ⛔ GUARD UTAMA: tolak jika URL sekarang bukan URL saat capture
if (!isOnSameChat()) {
console.warn(`[Webcam] Paste dibatalkan — capture di "${capturedOnUrl}", sekarang di "${getCurrentChatUrl()}"`);
return false;
}
const file = new File([blob], "capture.png", { type: "image/png" });
let success = false;
// Strategi 1: DragEvent pada dropzone Angular
const dropzone = document.querySelector('[xapfileselectordropzone]');
if (dropzone) {
try {
const dt = new DataTransfer();
dt.items.add(file);
dropzone.dispatchEvent(new DragEvent('dragenter', { bubbles: true, cancelable: true }));
dropzone.dispatchEvent(new DragEvent('dragover', { dataTransfer: dt, bubbles: true, cancelable: true }));
dropzone.dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true, cancelable: true }));
success = true;
console.log('[Webcam] Strategi 1 (DragEvent) berhasil');
} catch (e) {
console.warn('[Webcam] Strategi 1 gagal:', e);
}
}
// Strategi 2: Hidden file input Gemini
if (!success) {
const hiddenInput = document.querySelector('[data-test-id="hidden-local-image-upload-button"]');
if (hiddenInput) {
try {
const tempInput = document.createElement('input');
tempInput.type = 'file';
tempInput.accept = 'image/*';
tempInput.style.cssText = 'position:fixed; top:-9999px; left:-9999px;';
document.body.appendChild(tempInput);
const dt = new DataTransfer();
dt.items.add(file);
tempInput.files = dt.files;
tempInput.dispatchEvent(new Event('change', { bubbles: true }));
document.body.removeChild(tempInput);
success = true;
console.log('[Webcam] Strategi 2 (hidden input) berhasil');
} catch (e) {
console.warn('[Webcam] Strategi 2 gagal:', e);
}
}
}
// Strategi 3: ClipboardEvent ke Quill/contenteditable
if (!success) {
const chatInput = document.querySelector('.ql-editor.textarea') ||
document.querySelector('div[contenteditable="true"][role="textbox"]');
if (chatInput) {
try {
chatInput.focus();
const dt = new DataTransfer();
dt.items.add(file);
chatInput.dispatchEvent(new ClipboardEvent('paste', {
clipboardData: dt,
bubbles: true,
cancelable: true
}));
success = true;
console.log('[Webcam] Strategi 3 (ClipboardEvent) berhasil');
} catch (e) {
console.warn('[Webcam] Strategi 3 gagal:', e);
}
}
}
if (success) {
// Setelah paste sukses, hapus referensi URL agar tidak bisa paste lagi
capturedOnUrl = null;
await clearClipboard();
} else {
statusText.textContent = '❌ Semua strategi gagal. Coba upload manual via tombol +';
console.error('[Webcam] Semua strategi gagal');
}
return success;
};
// ── Timer & Capture ─────────────────────────────────────────────────────
captureBtn.addEventListener('click', () => {
if (!mediaStream || captureBtn.disabled) return;
captureBtn.disabled = true;
let timeLeft = 3;
captureBtn.style.backgroundColor = '#fbbc04';
captureBtn.textContent = `Merekam dalam ${timeLeft}...`;
const timer = setInterval(() => {
timeLeft--;
if (timeLeft <= 0) {
clearInterval(timer);
captureBtn.textContent = '📸 Mengambil...';
executeCapture();
} else {
captureBtn.textContent = `Merekam dalam ${timeLeft}...`;
}
}, 1000);
});
const executeCapture = () => {
// Simpan URL chat saat tombol capture ditekan
capturedOnUrl = getCurrentChatUrl();
console.log(`[Webcam] Foto diambil di: ${capturedOnUrl}`);
videoElement.style.opacity = '0.3';
setTimeout(() => videoElement.style.opacity = '1', 150);
canvasElement.width = videoElement.videoWidth;
canvasElement.height = videoElement.videoHeight;
const ctx = canvasElement.getContext('2d');
ctx.translate(canvasElement.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(videoElement, 0, 0, canvasElement.width, canvasElement.height);
canvasElement.toBlob(async (blob) => {
if (!blob) return;
const success = await autoPasteToGemini(blob);
if (success) {
setTimeout(() => {
closeCamera();
captureBtn.disabled = false;
captureBtn.style.backgroundColor = '#34a853';
captureBtn.textContent = '⏱️ Ambil (3 Detik)';
}, 300);
} else {
captureBtn.disabled = false;
captureBtn.style.backgroundColor = '#34a853';
captureBtn.textContent = '⏱️ Ambil (3 Detik)';
}
}, 'image/png');
};
})();