// Content script for Gemini integration console.log('Auto Cover Generator content script loaded on Gemini'); // Selectors for Gemini UI elements (may need adjustment based on Gemini's actual DOM) const SELECTORS = { inputArea: 'rich-textarea[placeholder*="Enter"], textarea[placeholder*="Enter"], div[contenteditable="true"]', sendButton: 'button[aria-label*="Send"], button[type="submit"]', imageUploadButton: 'button[aria-label*="Add"], input[type="file"]', generatedImage: 'img[src*="googleusercontent"], img[src*="generated"], img.image.loaded, img[alt="รูปภาพ"]', responseContainer: 'div[data-message-author-role="model"], .model-response' }; let isGenerating = false; let currentPrompt = null; let currentImage = null; let lastGenerationRequestTime = 0; // Timestamp of last extension-triggered generation // Listen for messages from side panel chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'generateCover' || message.action === 'removeObjectFromImage') { // Reuse the generation handler but adapt inputs const isRemover = message.action === 'removeObjectFromImage'; // For remover, the 'image' field is actually the 'imageData' (base64) // and we wrap it in an array to match logic. // For cover, 'images' or 'image' is used. const msgAdapter = isRemover ? { prompt: message.prompt, images: [message.imageData] // Specific for remover } : message; handleGenerateCover(msgAdapter) .then(() => sendResponse({ success: true })) .catch(error => sendResponse({ success: false, error: error.message })); return true; // Keep channel open for async response } }); // Main generation handler async function handleGenerateCover(message) { if (isGenerating) { throw new Error('กำลังสร้างอยู่แล้ว กรุณารอสักครู่'); } isGenerating = true; lastGenerationRequestTime = Date.now(); // Start the "Auto Download" window currentPrompt = message.prompt; currentImage = message.image; try { // Step 1: Find input area const inputElement = await waitForElement(SELECTORS.inputArea, 5000); if (!inputElement) { throw new Error('ไม่พบช่องกรอกข้อความของ Gemini'); } // Count existing images to differentiate new generation const existingImages = document.querySelectorAll(SELECTORS.generatedImage); const initialImageCount = existingImages.length; console.log(`Initial image count: ${initialImageCount}`); // Step 2: Upload images if provided const images = message.images || (message.image ? [message.image] : []); if (images.length > 0) { await uploadImagesToGemini(images, inputElement); // Wait for image upload processing // User requested delay consistent with number of attached images // Optimized: Reduced from 3000ms to 1000ms per image for speed const uploadDelay = Math.min(images.length * 1000, 12000); // Max 12s console.log(`Waiting ${uploadDelay}ms for ${images.length} images to upload/process...`); await sleep(uploadDelay); } // Step 3: Insert prompt await insertPrompt(inputElement, currentPrompt); await sleep(100); // Reduced from 500 // Step 3.5: Press Enter key await pressEnter(inputElement); await sleep(200); // Reduced from 500 // Step 4: Click send button with retry const sent = await clickSendButton(); if (sent) { // Wait a bit for Gemini to process the prompt await sleep(1000); // Reduced from 2000 // Step 4.5: Auto-click generate/create button if it appears await autoClickGenerateButton(); // Step 5: Wait for and capture generated image // Network Listener in background.js now handles detection and download! // await waitForGeneratedImage(initialImageCount); console.log('Content: Waiting for background network detection...'); } else { throw new Error('ไม่สามารถกดปุ่มส่งข้อความได้'); } } catch (error) { console.error('Generation error:', error); chrome.runtime.sendMessage({ action: 'generationError', error: error.message }); } finally { isGenerating = false; } } // Click send button with retry async function clickSendButton() { const maxRetries = 10; for (let i = 0; i < maxRetries; i++) { const sendButton = document.querySelector(SELECTORS.sendButton); if (sendButton && !sendButton.disabled && sendButton.getAttribute('aria-disabled') !== 'true') { console.log('Found enabled send button, clicking...'); sendButton.click(); return true; } console.log('Waiting for send button to be enabled...'); await sleep(500); } return false; } // Simulate Enter key press async function pressEnter(element) { console.log('Simulating Enter key...'); const events = ['keydown', 'keypress', 'keyup']; for (const type of events) { const event = new KeyboardEvent(type, { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13, which: 13, char: '\r', view: window }); element.dispatchEvent(event); await sleep(50); } } // Insert prompt into Gemini input async function insertPrompt(element, prompt) { element.focus(); // Clear existing content if possible if (element.tagName === 'TEXTAREA' || element.tagName === 'INPUT') { element.value = ''; element.value = prompt; element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); } else { // ContentEditable div element.textContent = prompt; // Dispatch complex event sequence for rich text editors const events = ['keydown', 'keypress', 'textInput', 'input', 'keyup']; events.forEach(eventType => { const event = new Event(eventType, { bubbles: true, cancelable: true }); element.dispatchEvent(event); }); // Trigger specific InputEvent for modern frameworks try { const inputEvent = new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: prompt }); element.dispatchEvent(inputEvent); } catch (e) { // Fallback for older browsers } } // Small delay to let framework validation run await sleep(200); element.blur(); await sleep(100); element.focus(); } // Super Robust Upload Function: Tries Paste -> DragDrop -> Classical Input async function uploadImagesToGemini(imagesDatBase64, inputElement) { console.log('Starting hybrid upload sequence...'); // Helper: Base64 to Blob const base64ToBlob = (base64) => { const arr = base64.split(','); const mime = arr[0].match(/:(.*?);/)[1]; const bstr = atob(arr[1]); let n = bstr.length; const u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([new Uint8Array(u8arr)], 'image.png', { type: mime }); }; // Prepare files const dataTransfer = new DataTransfer(); for (let i = 0; i < imagesDatBase64.length; i++) { const file = base64ToBlob(imagesDatBase64[i]); dataTransfer.items.add(file); } const fileList = dataTransfer.files; // STRATEGY 1: DISPATCH PASTE EVENT try { console.log('Strategy 1: Simulating Paste Event...'); const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: dataTransfer }); // Target the specific input (textarea/div) first inputElement.dispatchEvent(pasteEvent); await sleep(500); } catch (e) { console.warn('Strategy 1 failed', e); } // STRATEGY 2: CLICK ADD BUTTON & ATTACH TO INPUT (Fall back to native interaction) console.log('Strategy 2: UI Interaction (Find & Click Add Button)...'); // 2.1 Find the Add Button // Scoped search near input let addBtn = null; let inputContainer = document.body; if (inputElement) { inputContainer = inputElement.closest('.textarea-wrapper, .input-area, .text-input-field, footer, .main-footer') || inputElement.parentElement.parentElement; } if (inputContainer) { // Updated selectors based on common Gemini UI and Material Design const selectors = [ 'button[aria-label*="Upload"]', 'button[aria-label*="Add"]', 'button[aria-label*="เลือกรูป"]', 'button[aria-label*="เพิ่ม"]', '.mat-mdc-button-touch-target', // Material touch target often overlays the icon 'button:has(mat-icon[data-mat-icon-name="add"])', 'button:has(svg)', // Risky but constrained to inputContainer ]; for (const sel of selectors) { const found = inputContainer.querySelectorAll(sel); for (const btn of found) { // Check if it looks like the plus button (usually first button on left) const rect = btn.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { addBtn = btn; break; } } if (addBtn) break; } } if (addBtn) { console.log('Found Add Button, clicking...', addBtn); addBtn.click(); await sleep(800); // Wait for menu animation } else { console.log('Could not pinpoint Add Button, skipping click step.'); } // 2.2 Find File Input (It should exist now if click worked, or be always present) const fileInput = document.querySelector('input[type="file"]'); if (fileInput) { console.log('Found File Input, attaching files...'); fileInput.files = fileList; fileInput.dispatchEvent(new Event('change', { bubbles: true })); fileInput.dispatchEvent(new Event('input', { bubbles: true })); console.log('Files attached to input.'); return true; } else { console.warn('Strategy 2 failed: No file input found even after clicking.'); } // STRATEGY 3: DRAG & DROP (Last Resort) console.log('Strategy 3: Drag & Drop Simulation...'); const dropEventInit = { bubbles: true, cancelable: true, view: window, dataTransfer: dataTransfer }; // Try to find specific dropzone if inputElement is just a text area const dropzone = document.querySelector('xapfileselectordropzone') || document.querySelector('.drop-zone') || document.body; let dropTarget = (dropzone && dropzone !== document.body) ? dropzone : inputElement; dropTarget.dispatchEvent(new DragEvent('dragenter', dropEventInit)); await sleep(50); dropTarget.dispatchEvent(new DragEvent('dragover', dropEventInit)); await sleep(50); dropTarget.dispatchEvent(new DragEvent('drop', dropEventInit)); console.log('Hybrid upload sequence completed.'); // We return true blindly because we tried everything. return true; } // Auto-click generate/create button async function autoClickGenerateButton() { // Common selectors for Gemini's generate/create button const generateButtonSelectors = [ 'button[aria-label*="Generate"]', 'button[aria-label*="Create"]', 'button:has-text("Generate")', 'button:has-text("Create")', 'button[data-test-id*="generate"]', 'button[data-test-id*="create"]', '.generate-button', '.create-button' ]; // Try to find and click the generate button in a loop for a short period const maxAttempts = 5; for (let i = 0; i < maxAttempts; i++) { for (const selector of generateButtonSelectors) { try { const button = document.querySelector(selector); if (button && button.offsetParent !== null) { // Check if visible console.log('Found generate button, clicking...', selector); button.click(); await sleep(500); return true; } } catch (e) { // Continue to next selector } } await sleep(500); } // Alternative: Look for buttons with specific text content const allButtons = document.querySelectorAll('button'); for (const button of allButtons) { const text = button.textContent.toLowerCase(); if ((text.includes('generate') || text.includes('create') || text.includes('สร้าง')) && button.offsetParent !== null) { console.log('Found generate button by text, clicking...', button.textContent); button.click(); await sleep(500); return true; } } console.log('Generate button not found, image may need manual generation'); return false; } // Wait for generated image to appear async function waitForGeneratedImage(initialCount = 0) { const maxWaitTime = 60000; // 60 seconds const checkInterval = 1000; // Check every second let elapsed = 0; return new Promise((resolve, reject) => { const interval = setInterval(() => { elapsed += checkInterval; // Look for generated images const images = document.querySelectorAll(SELECTORS.generatedImage); if (images.length > initialCount) { // Get the most recent image const latestImage = images[images.length - 1]; const imageUrl = latestImage.src; if (imageUrl && !imageUrl.includes('data:image')) { clearInterval(interval); // Send image URL back to side panel chrome.runtime.sendMessage({ action: 'imageGenerated', imageUrl: imageUrl }); resolve(imageUrl); } } if (elapsed >= maxWaitTime) { clearInterval(interval); reject(new Error('หมดเวลารอรูปภาพจาก Gemini')); } }, checkInterval); }); } // Utility: Wait for element to appear function waitForElement(selector, timeout = 5000) { return new Promise((resolve) => { const element = document.querySelector(selector); if (element) { resolve(element); return; } const observer = new MutationObserver(() => { const element = document.querySelector(selector); if (element) { observer.disconnect(); resolve(element); } }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { observer.disconnect(); resolve(null); }, timeout); }); } // Utility: Sleep function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Monitor for manual image generation (if user generates without extension) // ----------------------------------------------------------- // ----------------------------------------------------------- // Auto Download Logic (Persistent) // ----------------------------------------------------------- let isAutoDownloadEnabled = false; // Default off, will sync with storage const downloadedImages = new Set(); // Local cache of what we've seen/downloaded // Initialize state from storage chrome.storage.local.get(['autoDownload', 'downloadedHistory'], (result) => { isAutoDownloadEnabled = result.autoDownload || false; // 1. Load history into Set if (result.downloadedHistory && Array.isArray(result.downloadedHistory)) { result.downloadedHistory.forEach(url => downloadedImages.add(url)); } // 2. Scan Existing DOM for images (The "Reload Extension" case) // Any image currently visible is "old" and shouldn't be auto-downloaded again this session // We add EVERYTHING currently on screen to the history to be safe. const existingImages = document.querySelectorAll('img'); let addedCount = 0; existingImages.forEach(img => { if (img.src && img.src.startsWith('http')) { // Just add it blindly to history so we don't process it again if (!downloadedImages.has(img.src)) { downloadedImages.add(img.src); addedCount++; } } }); if (addedCount > 0) { console.log(`Marked ${addedCount} existing images as seen (skipped download).`); saveDownloadHistory(); } console.log('Auto Download status:', isAutoDownloadEnabled); console.log(`Loaded ${downloadedImages.size} total images in history.`); // Start observing ONLY if enabled if (isAutoDownloadEnabled) { startObserving(); } }); // Listen for storage changes chrome.storage.onChanged.addListener((changes, area) => { if (area === 'local' && changes.autoDownload) { isAutoDownloadEnabled = changes.autoDownload.newValue; console.log('Auto Download status changed to:', isAutoDownloadEnabled); if (isAutoDownloadEnabled) { startObserving(); } else { stopObserving(); } } }); // ... (previous code) let observerActive = false; let isInitialLoad = true; // Set a grace period for the initial load // This prevents the extension from downloading existing chat history as "new" images // when the user reloads the page. setTimeout(() => { isInitialLoad = false; console.log('Initial load grace period ended. Ready to capture NEW images.'); }, 5000); const autoDownloadObserver = new MutationObserver((mutations) => { // Only run if we are generally interested, though checking inside loop is safer for dynamic toggling for (const mutation of mutations) { for (const node of mutation.addedNodes) { // Specific validaiton for IMG tag if (node.tagName === 'IMG') { checkAndDownloadImage(node); } // Sometimes images are nested in added containers if (node.querySelectorAll) { const images = node.querySelectorAll('img'); images.forEach(checkAndDownloadImage); } } } }); let saveTimeout; function debouncedSaveHistory() { clearTimeout(saveTimeout); saveTimeout = setTimeout(saveDownloadHistory, 1000); } function checkAndDownloadImage(img) { // 1. Check if it's already processed (in local Set) // Safety: Duplicate Check using src as key if (downloadedImages.has(img.src)) return; // 2. Wait for image to load to check dimensions if (!img.complete) { img.onload = () => checkAndDownloadImage(img); return; } if (!img.src) return; // --- STRICT FILTERING LOGIC --- // Condition A (Size): Width OR Height > 300px // Prevents icons, avatars, stickers const isBigEnough = img.naturalWidth > 300 || img.naturalHeight > 300; if (!isBigEnough) { return; } // Condition B (Source): // - URL must not be small Data URI (handled partly by size check, but user wants strict source check) // - Must not be Blob from user upload // - If possible, check if in model-response container // Check ancestors for container type const messageContainer = img.closest('[data-message-author-role], .model-response, .user-message, .query-container'); let isModelResponse = false; let isUserUpload = false; if (messageContainer) { const role = messageContainer.getAttribute('data-message-author-role'); if (role === 'user') { isUserUpload = true; } else if (role === 'model') { isModelResponse = true; // High confidence } else if (messageContainer.classList.contains('user-message') || messageContainer.classList.contains('query-container')) { isUserUpload = true; } else if (messageContainer.classList.contains('model-response')) { isModelResponse = true; } } // B.1: Reject Explicit User Uploads if (isUserUpload) { console.log('Ignored: Image is in User message', img.src); downloadedImages.add(img.src); return; } // B.2: Source URL Check const src = img.src; const isBlob = src.startsWith('blob:'); const isData = src.startsWith('data:'); if ((isBlob || isData) && !isModelResponse) { console.log('Ignored: Blob/Data URI without Model context (Likely user upload)', src); return; } // B.3: Input Area Check (Composing phrase) if (img.closest('div[contenteditable="true"], .input-area, textarea, .text-input-field')) { return; } // B.4: Avatar Check (googleusercontent specific) if (src.includes('googleusercontent.com/a/')) { return; } // If we passed all filters: // console.log('Pass: Valid Generated Image detected:', src); // Add to handled set immediately downloadedImages.add(src); debouncedSaveHistory(); // Use debounced save // CRITICAL: Check if we are in the initial load phase (Reload protection) if (isInitialLoad) { console.log('Skipped Download (Initial Load):', src); return; } if (isAutoDownloadEnabled) { console.log('Auto-downloading image...'); chrome.runtime.sendMessage({ action: 'downloadImage', url: src }); } } function saveDownloadHistory() { // Convert Set to Array const historyArray = Array.from(downloadedImages); // Limit history size to prevent storage bloat (e.g., last 500 images) if (historyArray.length > 500) { historyArray.splice(0, historyArray.length - 500); } chrome.storage.local.set({ downloadedHistory: historyArray }); } function startObserving() { if (observerActive) return; // Prevent double attach // Start observing specifically for images appearing in the chat autoDownloadObserver.observe(document.body, { childList: true, subtree: true }); observerActive = true; console.log('Auto Download Observer STARTED'); } function stopObserving() { if (!observerActive) return; autoDownloadObserver.disconnect(); observerActive = false; console.log('Auto Download Observer STOPPED (Resource Saved)'); }