Gemini Webcam Capture

Webcam capture

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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');
    };

})();