@semantest/chrome-extension
Version:
Browser extension for ChatGPT-buddy - AI automation extension built on Web-Buddy framework
445 lines (440 loc) • 15.7 kB
JavaScript
/*
Web-Buddy Framework
Google Images Content Script Adapter
Copyright (C) 2025-today rydnr@acm-sl.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Adapter } from '@typescript-eda/core';
import { GoogleImageDownloadRequested, FileDownloadRequested } from '../../domain/events/download-events';
import { GoogleImagesDownloader } from '../../domain/entities/google-images-downloader';
/**
* Google Images Content Adapter - Handles Google Images page interactions
*
* Responsibilities:
* - Detect Google Images page and inject download functionality
* - Handle image right-click and download requests
* - Monitor for new images loaded dynamically
* - Integrate with training system for pattern learning
*/
export class GoogleImagesContentAdapter extends Adapter {
constructor() {
super();
this.isGoogleImagesPage = false;
this.downloadButtons = new Map();
this.observer = null;
this.downloader = new GoogleImagesDownloader();
this.initialize();
}
/**
* Initializes the adapter and checks if we're on Google Images
*/
initialize() {
this.isGoogleImagesPage = this.detectGoogleImagesPage();
if (this.isGoogleImagesPage) {
this.setupGoogleImagesIntegration();
}
}
/**
* Detects if current page is Google Images
*/
detectGoogleImagesPage() {
const hostname = window.location.hostname;
const pathname = window.location.pathname;
const search = window.location.search;
return (hostname.includes('google.') &&
(pathname.includes('/imghp') ||
pathname.includes('/search') && search.includes('tbm=isch'))) ||
document.title.toLowerCase().includes('google images');
}
/**
* Sets up Google Images specific functionality
*/
setupGoogleImagesIntegration() {
this.addDownloadButtonsToImages();
this.setupImageMonitoring();
this.addContextMenuHandlers();
this.setupKeyboardShortcuts();
}
/**
* Adds download buttons to visible images
*/
addDownloadButtonsToImages() {
const images = this.findGoogleImages();
images.forEach(img => {
if (!this.downloadButtons.has(img)) {
this.addDownloadButton(img);
}
});
}
/**
* Finds Google Images on the page
*/
findGoogleImages() {
const selectors = [
'img[data-src]', // Lazy loaded images
'img[src*="googleusercontent.com"]', // Google hosted images
'img[src*="gstatic.com"]', // Google static images
'.islrc img', // Images in search results
'.rg_i', // Result grid images
'img[jsname]' // Google's internal images
];
const images = [];
selectors.forEach(selector => {
const found = document.querySelectorAll(selector);
images.push(...Array.from(found));
});
// Filter for actual images
return images.filter(img => {
return img.src &&
(img.naturalWidth > 50 || img.width > 50) && // Minimum size
(img.naturalHeight > 50 || img.height > 50);
});
}
/**
* Adds download button to individual image
*/
addDownloadButton(img) {
const container = this.createImageContainer(img);
const downloadButton = this.createDownloadButton(img);
container.appendChild(downloadButton);
this.downloadButtons.set(img, downloadButton);
// Position the container over the image
this.positionDownloadButton(img, container);
}
/**
* Creates container for download button
*/
createImageContainer(img) {
const container = document.createElement('div');
container.className = 'web-buddy-download-container';
container.style.cssText = `
position: absolute;
top: 0;
right: 0;
z-index: 10000;
pointer-events: none;
`;
// Insert container relative to image
img.parentElement?.appendChild(container);
return container;
}
/**
* Creates download button for image
*/
createDownloadButton(img) {
const button = document.createElement('button');
button.innerHTML = '📥'; // Download emoji
button.title = 'Download Image with Web-Buddy';
button.className = 'web-buddy-download-btn';
button.style.cssText = `
background: rgba(0, 0, 0, 0.8);
color: white;
border: none;
border-radius: 4px;
width: 32px;
height: 32px;
cursor: pointer;
font-size: 16px;
margin: 4px;
pointer-events: auto;
transition: background-color 0.2s;
`;
// Hover effects
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = 'rgba(0, 0, 0, 1)';
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
});
// Click handler
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.handleImageDownload(img);
});
return button;
}
/**
* Positions download button relative to image
*/
positionDownloadButton(img, container) {
const updatePosition = () => {
const imgRect = img.getBoundingClientRect();
const parentRect = img.parentElement?.getBoundingClientRect();
if (parentRect) {
container.style.left = `${imgRect.right - parentRect.left - 40}px`;
container.style.top = `${imgRect.top - parentRect.top + 4}px`;
}
};
// Initial positioning
updatePosition();
// Update position on scroll/resize
window.addEventListener('scroll', updatePosition);
window.addEventListener('resize', updatePosition);
}
/**
* Sets up monitoring for dynamically loaded images
*/
setupImageMonitoring() {
this.observer = new MutationObserver((mutations) => {
let newImagesFound = false;
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node instanceof HTMLImageElement ||
(node instanceof HTMLElement && node.querySelector('img'))) {
newImagesFound = true;
}
});
}
});
if (newImagesFound) {
// Debounce to avoid excessive calls
setTimeout(() => {
this.addDownloadButtonsToImages();
}, 500);
}
});
this.observer.observe(document.body, {
childList: true,
subtree: true
});
}
/**
* Adds context menu handlers for images
*/
addContextMenuHandlers() {
document.addEventListener('contextmenu', (e) => {
const target = e.target;
if (target instanceof HTMLImageElement && this.isGoogleImage(target)) {
this.showCustomContextMenu(e, target);
}
});
}
/**
* Checks if image is a Google Images result
*/
isGoogleImage(img) {
return img.src.includes('googleusercontent.com') ||
img.src.includes('gstatic.com') ||
img.closest('.islrc, .rg_i') !== null ||
img.hasAttribute('jsname');
}
/**
* Shows custom context menu with download option
*/
showCustomContextMenu(e, img) {
e.preventDefault();
const menu = document.createElement('div');
menu.className = 'web-buddy-context-menu';
menu.style.cssText = `
position: fixed;
top: ${e.clientY}px;
left: ${e.clientX}px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10001;
min-width: 200px;
`;
const downloadOption = document.createElement('div');
downloadOption.innerHTML = '📥 Download with Web-Buddy';
downloadOption.style.cssText = `
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #eee;
`;
downloadOption.addEventListener('click', () => {
this.handleImageDownload(img);
document.body.removeChild(menu);
});
downloadOption.addEventListener('mouseenter', () => {
downloadOption.style.backgroundColor = '#f5f5f5';
});
downloadOption.addEventListener('mouseleave', () => {
downloadOption.style.backgroundColor = 'white';
});
menu.appendChild(downloadOption);
document.body.appendChild(menu);
// Remove menu on click outside
const removeMenu = (e) => {
if (!menu.contains(e.target)) {
document.body.removeChild(menu);
document.removeEventListener('click', removeMenu);
}
};
setTimeout(() => {
document.addEventListener('click', removeMenu);
}, 100);
}
/**
* Sets up keyboard shortcuts for downloads
*/
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl+Shift+D to download focused/hovered image
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
e.preventDefault();
const hoveredImg = this.findHoveredImage();
if (hoveredImg) {
this.handleImageDownload(hoveredImg);
}
}
});
}
/**
* Finds currently hovered image
*/
findHoveredImage() {
const elements = document.querySelectorAll(':hover');
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
if (element instanceof HTMLImageElement && this.isGoogleImage(element)) {
return element;
}
}
return null;
}
/**
* Handles image download request
*/
async handleImageDownload(img) {
try {
// Extract search query from page
const searchQuery = this.extractSearchQuery();
// Create download request event
const downloadEvent = new GoogleImageDownloadRequested(this.generateCorrelationId(), {
src: img.src,
alt: img.alt,
title: img.title,
width: img.naturalWidth || img.width,
height: img.naturalHeight || img.height
}, searchQuery);
// Show download feedback
this.showDownloadFeedback(img, 'Downloading...');
// Process through downloader entity
const result = await this.downloader.downloadGoogleImage(downloadEvent);
if (result instanceof FileDownloadRequested) {
// Send to background script for actual download
this.sendToBackground({
action: 'DOWNLOAD_FILE',
payload: {
url: result.url,
filename: result.filename,
conflictAction: result.conflictAction,
saveAs: result.saveAs
},
correlationId: downloadEvent.correlationId
});
this.showDownloadFeedback(img, 'Download started!');
}
else {
this.showDownloadFeedback(img, 'Download failed!', true);
}
}
catch (error) {
console.error('Google Images download error:', error);
this.showDownloadFeedback(img, 'Download error!', true);
}
}
/**
* Extracts current search query from Google Images page
*/
extractSearchQuery() {
// Try to get search query from URL
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get('q');
if (query) {
return query;
}
// Try to get from search input
const searchInput = document.querySelector('input[name="q"]');
if (searchInput && searchInput.value) {
return searchInput.value;
}
// Try to get from page title
const title = document.title;
const match = title.match(/^(.+?)\s*-\s*Google/);
if (match) {
return match[1];
}
return 'google_search';
}
/**
* Shows visual feedback for download status
*/
showDownloadFeedback(img, message, isError = false) {
const feedback = document.createElement('div');
feedback.textContent = message;
feedback.style.cssText = `
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: ${isError ? '#f44336' : '#4CAF50'};
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 10002;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s;
`;
// Position relative to image
const imgContainer = img.parentElement;
if (imgContainer) {
imgContainer.style.position = 'relative';
imgContainer.appendChild(feedback);
// Animate in
setTimeout(() => {
feedback.style.opacity = '1';
}, 100);
// Remove after delay
setTimeout(() => {
feedback.style.opacity = '0';
setTimeout(() => {
if (feedback.parentElement) {
feedback.parentElement.removeChild(feedback);
}
}, 300);
}, 2000);
}
}
/**
* Sends message to background script
*/
sendToBackground(message) {
chrome.runtime.sendMessage(message);
}
/**
* Generates unique correlation ID
*/
generateCorrelationId() {
return `google-images-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Cleanup method
*/
dispose() {
if (this.observer) {
this.observer.disconnect();
}
this.downloadButtons.clear();
// Remove all download containers
const containers = document.querySelectorAll('.web-buddy-download-container');
containers.forEach(container => {
container.parentElement?.removeChild(container);
});
}
}