/**
* ds-screenshot-gallery.js
* Screenshot gallery with IndexedDB storage and artifact-based images
*
* REFACTORED: DSS-compliant version using DSBaseTool + gallery-template.js
* - Extends DSBaseTool for Shadow DOM, AbortController, and standardized lifecycle
* - Uses gallery-template.js for DSS-compliant templating (NO inline events/styles)
* - Event delegation pattern for all interactions
* - Logger utility instead of console.*
*
* Reference: .knowledge/dss-coding-standards.json
*/
import DSBaseTool from '../base/ds-base-tool.js';
import toolBridge from '../../services/tool-bridge.js';
import { ComponentHelpers } from '../../utils/component-helpers.js';
import { logger } from '../../utils/logger.js';
class DSScreenshotGallery extends DSBaseTool {
constructor() {
super();
this.screenshots = [];
this.selectedScreenshot = null;
this.isCapturing = false;
this.db = null;
}
async connectedCallback() {
// Initialize IndexedDB first
await this.initDB();
// Call parent connectedCallback (renders + setupEventListeners)
super.connectedCallback();
// Load screenshots after render
await this.loadScreenshots();
}
/**
* Initialize IndexedDB for metadata storage
*/
async initDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('ds-screenshots', 1);
request.onerror = () => {
logger.error('[DSScreenshotGallery] Failed to open IndexedDB', request.error);
reject(request.error);
};
request.onsuccess = () => {
this.db = request.result;
logger.debug('[DSScreenshotGallery] IndexedDB initialized');
resolve();
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('screenshots')) {
const store = db.createObjectStore('screenshots', { keyPath: 'id' });
store.createIndex('timestamp', 'timestamp', { unique: false });
store.createIndex('tags', 'tags', { unique: false, multiEntry: true });
logger.info('[DSScreenshotGallery] IndexedDB schema created');
}
};
});
}
/**
* Render the component (required by DSBaseTool)
*/
render() {
this.shadowRoot.innerHTML = `
`;
}
/**
* Setup event listeners (required by DSBaseTool)
* Uses event delegation pattern with data-action attributes
*/
setupEventListeners() {
// EVENT-002: Event delegation on container
this.delegateEvents('.screenshot-gallery-container', 'click', (action, e) => {
switch (action) {
case 'capture':
this.captureScreenshot();
break;
case 'item-click':
const idx = parseInt(e.target.closest('[data-item-idx]')?.dataset.itemIdx, 10);
if (!isNaN(idx) && this.screenshots[idx]) {
this.viewScreenshot(this.screenshots[idx]);
}
break;
case 'item-delete':
const deleteIdx = parseInt(e.target.closest('[data-item-idx]')?.dataset.itemIdx, 10);
if (!isNaN(deleteIdx) && this.screenshots[deleteIdx]) {
this.handleDelete(this.screenshots[deleteIdx].id);
}
break;
}
});
}
async captureScreenshot() {
if (this.isCapturing) return;
this.isCapturing = true;
const captureBtn = this.$('#capture-screenshot-btn');
if (captureBtn) {
captureBtn.disabled = true;
captureBtn.textContent = '📸 Capturing...';
}
try {
const selectorInput = this.$('#screenshot-selector');
const fullPageToggle = this.$('#screenshot-fullpage');
const selector = selectorInput?.value.trim() || null;
const fullPage = fullPageToggle?.checked || false;
logger.info('[DSScreenshotGallery] Capturing screenshot', { selector, fullPage });
// Call MCP tool to capture screenshot
const result = await toolBridge.takeScreenshot(fullPage, selector);
if (result && result.screenshot) {
// Save metadata to IndexedDB
const screenshot = {
id: Date.now(),
timestamp: new Date(),
selector: selector || 'Full Page',
fullPage,
imageData: result.screenshot, // Base64 image data
tags: selector ? [selector] : ['fullpage']
};
await this.saveScreenshot(screenshot);
await this.loadScreenshots();
ComponentHelpers.showToast?.('Screenshot captured successfully', 'success');
logger.info('[DSScreenshotGallery] Screenshot saved', { id: screenshot.id });
} else {
throw new Error('No screenshot data returned');
}
} catch (error) {
logger.error('[DSScreenshotGallery] Failed to capture screenshot', error);
ComponentHelpers.showToast?.(`Failed to capture screenshot: ${error.message}`, 'error');
} finally {
this.isCapturing = false;
if (captureBtn) {
captureBtn.disabled = false;
captureBtn.textContent = '📸 Capture';
}
}
}
async saveScreenshot(screenshot) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['screenshots'], 'readwrite');
const store = transaction.objectStore('screenshots');
const request = store.add(screenshot);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async loadScreenshots() {
const content = this.$('#gallery-content');
if (!content) return;
try {
this.screenshots = await this.getAllScreenshots();
logger.debug('[DSScreenshotGallery] Loaded screenshots', { count: this.screenshots.length });
this.renderGallery();
} catch (error) {
logger.error('[DSScreenshotGallery] Failed to load screenshots', error);
content.innerHTML = ComponentHelpers.renderError('Failed to load screenshots', error);
}
}
async getAllScreenshots() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['screenshots'], 'readonly');
const store = transaction.objectStore('screenshots');
const request = store.getAll();
request.onsuccess = () => resolve(request.result.reverse()); // Most recent first
request.onerror = () => reject(request.error);
});
}
async deleteScreenshot(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['screenshots'], 'readwrite');
const store = transaction.objectStore('screenshots');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async handleDelete(id) {
if (!confirm('Delete this screenshot?')) return;
try {
await this.deleteScreenshot(id);
await this.loadScreenshots();
ComponentHelpers.showToast?.('Screenshot deleted', 'success');
logger.info('[DSScreenshotGallery] Screenshot deleted', { id });
} catch (error) {
logger.error('[DSScreenshotGallery] Failed to delete screenshot', error);
ComponentHelpers.showToast?.(`Failed to delete: ${error.message}`, 'error');
}
}
viewScreenshot(screenshot) {
this.selectedScreenshot = screenshot;
this.renderModal();
}
renderModal() {
if (!this.selectedScreenshot) return;
// Create modal in Shadow DOM
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.innerHTML = `
`;
// Add click handlers for modal
this.bindEvent(modal, 'click', (e) => {
const closeBtn = e.target.closest('[data-action="close-modal"]');
if (closeBtn || e.target === modal) {
modal.remove();
this.selectedScreenshot = null;
logger.debug('[DSScreenshotGallery] Modal closed');
}
});
this.shadowRoot.appendChild(modal);
logger.debug('[DSScreenshotGallery] Modal opened', { id: this.selectedScreenshot.id });
}
renderGallery() {
const content = this.$('#gallery-content');
if (!content) return;
if (this.screenshots.length === 0) {
content.innerHTML = `
📸
No screenshots captured yet
`;
return;
}
// Transform screenshots to gallery items format
const galleryItems = this.screenshots.map(screenshot => ({
src: screenshot.imageData,
title: screenshot.selector,
subtitle: ComponentHelpers.formatRelativeTime(new Date(screenshot.timestamp))
}));
// Use DSS-compliant gallery template (NO inline styles/events)
// Note: We're using a simplified inline version here since we're in Shadow DOM
// For full modular approach, we'd import createGalleryView from gallery-template.js
content.innerHTML = `
${this.screenshots.length} screenshot${this.screenshots.length !== 1 ? 's' : ''} stored
${galleryItems.map((item, idx) => `
${this.escapeHtml(item.title)}
${item.subtitle}
🗑️ Delete
`).join('')}
`;
// Add hover styles via adoptedStyleSheets
this.adoptStyles(`
.gallery-item:hover {
transform: scale(1.02);
}
.gallery-item button:hover {
background: rgba(244, 135, 113, 0.2);
}
`);
logger.debug('[DSScreenshotGallery] Gallery rendered', { count: this.screenshots.length });
}
}
customElements.define('ds-screenshot-gallery', DSScreenshotGallery);
export default DSScreenshotGallery;