/** * 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) => ` `).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;