// DIDI Content Extractor - Content Script // Extracts content from social media posts // Facebook Extractor class FacebookExtractor { constructor() { this.platform = 'facebook'; } findPostContainer(element) { let current = element; let depth = 0; const maxDepth = 30; console.log('[Facebook] Starting search for post container from:', element.tagName); while (current && depth < maxDepth) { if (current.matches) { // Standard Feed posts if (current.matches('[role="article"]')) { console.log('[Facebook] Found article container at depth:', depth); return current; } // Groups posts, Stories, alternative layouts if (current.matches('div[data-pagelet*="FeedUnit"]') || current.matches('div[class*="userContentWrapper"]')) { console.log('[Facebook] Found pagelet container at depth:', depth); return current; } // Look for divs that contain both author and content // This is more generic and works across different FB layouts const hasAuthor = current.querySelector('h2 a[role="link"], h3 a[role="link"], h4 a[role="link"]'); const hasContent = current.querySelector('div[dir="auto"]'); if (hasAuthor && hasContent && depth > 3) { console.log('[Facebook] Found generic post container at depth:', depth); return current; } } current = current.parentElement; depth++; } console.log('[Facebook] No post container found after searching', maxDepth, 'levels'); return null; } extractAuthor(postElement) { const authorSelectors = [ 'h2 a[role="link"]', 'h3 a[role="link"]', 'a[aria-label*="profile"]', 'strong > a' ]; for (const selector of authorSelectors) { const authorEl = postElement.querySelector(selector); if (authorEl && authorEl.textContent.trim()) { const authorName = authorEl.textContent.trim(); console.log('[FB] Found author:', authorName); return authorName; } } console.log('[FB] Author not found'); return 'Unknown'; } extractText(postElement) { const textParts = []; const seenTexts = new Set(); const textSelectors = [ 'div[dir="auto"][style*="text-align"]', '[data-ad-comet-preview="message"]', 'div[data-ad-preview="message"]' ]; for (const selector of textSelectors) { const elements = postElement.querySelectorAll(selector); elements.forEach(el => { const text = el.textContent.trim(); const isUIText = [ 'Like', 'Comment', 'Share', 'Send', 'Vezi mai mult', 'See more', 'Show more', 'Just now', 'min', 'hr', 'hrs', '·', '•' ].some(noise => text === noise || text.length < 3); if (!isUIText && !seenTexts.has(text) && text.length > 10) { const hasNoTextChildren = Array.from(el.children).every(child => !child.textContent || child.textContent.trim().length === 0 ); if (hasNoTextChildren || text.length > 50) { textParts.push(text); seenTexts.add(text); } } }); } const fullText = textParts.join('\n\n').trim(); console.log('[FB] Extracted text length:', fullText.length); return fullText; } // Extract ALL images from post (not just primary) extractAllImages(postElement) { const images = postElement.querySelectorAll('img[src]'); const extractedImages = []; console.log('[FB] Found total images:', images.length); for (const img of images) { const src = img.src || img.getAttribute('data-src'); const width = img.naturalWidth || img.width; const height = img.naturalHeight || img.height; // Filter out emoji, icons, avatars, profile pics const isValidImage = src && !src.includes('emoji') && !src.includes('icon') && !src.includes('avatar') && !src.includes('profile') && width >= 200 && height >= 200 && !img.closest('a[aria-label*="profile"]'); if (isValidImage) { extractedImages.push({ url: src, width: width, height: height, alt: img.alt || '' }); } } console.log('[FB] Extracted valid images:', extractedImages.length); return extractedImages; } // Keep for backward compatibility extractPrimaryImage(postElement) { const allImages = this.extractAllImages(postElement); return allImages.length > 0 ? allImages[0] : null; } extractTimestamp(postElement) { const timeSelectors = [ 'a[aria-label*="ago"]', 'a[aria-label*="min"]', 'a[aria-label*="hour"]', 'abbr', 'span[title]' ]; for (const selector of timeSelectors) { const timeEl = postElement.querySelector(selector); if (timeEl) { const timestamp = timeEl.getAttribute('title') || timeEl.getAttribute('aria-label') || timeEl.textContent; console.log('[FB] Found timestamp:', timestamp); return timestamp; } } return new Date().toISOString(); } extractPostUrl(postElement) { const permalinkSelectors = [ 'a[aria-label*="ago"]', 'a[href*="/posts/"]', 'a[href*="/photos/"]', 'a[href*="/videos/"]' ]; for (const selector of permalinkSelectors) { const link = postElement.querySelector(selector); if (link && link.href) { console.log('[FB] Found post URL:', link.href); return link.href; } } return window.location.href; } // Extract all external links from post extractLinks(postElement) { const links = []; const anchorElements = postElement.querySelectorAll('a[href]'); anchorElements.forEach(anchor => { const href = anchor.href; // Filter out internal Facebook navigation links const isExternalLink = href && !href.includes('/profile/') && !href.includes('/groups/') && !href.includes('/hashtag/') && !href.includes('facebook.com/photo') && !href.includes('facebook.com/watch') && (href.startsWith('http://') || href.startsWith('https://')) && !href.includes('facebook.com'); if (isExternalLink && !links.includes(href)) { links.push(href); } }); console.log('[FB] Extracted external links:', links.length); return links; } extract(element) { console.log('[FB] Starting Facebook extraction...'); const postContainer = this.findPostContainer(element); if (!postContainer) { console.log('[FB] No post container found, returning null'); return null; } const author = this.extractAuthor(postContainer); const text = this.extractText(postContainer); const allImages = this.extractAllImages(postContainer); const primaryImage = allImages.length > 0 ? allImages[0] : null; const links = this.extractLinks(postContainer); const timestamp = this.extractTimestamp(postContainer); const postUrl = this.extractPostUrl(postContainer); let contentType = 'text'; if (text && allImages.length > 0) { contentType = 'text+image'; } else if (allImages.length > 0) { contentType = 'image'; } const result = { platform: this.platform, type: contentType, author: author, text: text, image: primaryImage, // Keep for backward compatibility images: allImages, // NEW: All images links: links, // NEW: External links timestamp: timestamp, postUrl: postUrl, extractedAt: new Date().toISOString() }; console.log('[FB] Extraction complete:', { type: result.type, author: result.author, textLength: result.text.length, imageCount: allImages.length, linkCount: links.length }); return result; } } // Twitter Extractor class TwitterExtractor { constructor() { this.platform = 'twitter'; } findPostContainer(element) { let current = element; let depth = 0; const maxDepth = 20; while (current && depth < maxDepth) { if (current.matches && current.matches('article[data-testid="tweet"]')) { console.log('[Twitter] Found tweet container at depth:', depth); return current; } current = current.parentElement; depth++; } console.log('[Twitter] No tweet container found'); return null; } extractAuthor(tweetElement) { const authorSelectors = [ '[data-testid="User-Name"] span', 'a[role="link"] span' ]; for (const selector of authorSelectors) { const authorEl = tweetElement.querySelector(selector); if (authorEl && authorEl.textContent.trim() && !authorEl.textContent.includes('@')) { const authorName = authorEl.textContent.trim(); console.log('[Twitter] Found author:', authorName); return authorName; } } return 'Unknown'; } extractText(tweetElement) { const textEl = tweetElement.querySelector('[data-testid="tweetText"]'); if (textEl) { const text = textEl.textContent.trim(); console.log('[Twitter] Extracted text length:', text.length); return text; } console.log('[Twitter] No text found'); return ''; } extractPrimaryImage(tweetElement) { const imgSelectors = [ '[data-testid="tweetPhoto"] img', 'img[alt*="Image"]' ]; for (const selector of imgSelectors) { const img = tweetElement.querySelector(selector); if (img && img.src) { console.log('[Twitter] Found primary image'); return { url: img.src, width: img.naturalWidth || img.width, height: img.naturalHeight || img.height, alt: img.alt || '' }; } } console.log('[Twitter] No primary image found'); return null; } extractTimestamp(tweetElement) { const timeEl = tweetElement.querySelector('time'); if (timeEl) { const timestamp = timeEl.getAttribute('datetime') || timeEl.textContent; console.log('[Twitter] Found timestamp:', timestamp); return timestamp; } return new Date().toISOString(); } extractPostUrl(tweetElement) { const timeLink = tweetElement.querySelector('time'); if (timeLink && timeLink.parentElement && timeLink.parentElement.href) { console.log('[Twitter] Found tweet URL:', timeLink.parentElement.href); return timeLink.parentElement.href; } return window.location.href; } extract(element) { console.log('[Twitter] Starting Twitter extraction...'); const tweetContainer = this.findPostContainer(element); if (!tweetContainer) { console.log('[Twitter] No tweet container found'); return null; } const author = this.extractAuthor(tweetContainer); const text = this.extractText(tweetContainer); const primaryImage = this.extractPrimaryImage(tweetContainer); const timestamp = this.extractTimestamp(tweetContainer); const postUrl = this.extractPostUrl(tweetContainer); let contentType = 'text'; if (text && primaryImage) { contentType = 'text+image'; } else if (primaryImage) { contentType = 'image'; } const result = { platform: this.platform, type: contentType, author: author, text: text, image: primaryImage, timestamp: timestamp, postUrl: postUrl, extractedAt: new Date().toISOString() }; console.log('[Twitter] Extraction complete:', { type: result.type, author: result.author, textLength: result.text.length, hasImage: !!result.image }); return result; } } let selectionBox = null; let isSelecting = false; let selectedElement = null; let currentPlatform = null; let extractor = null; // Snipping tool variables let isSnipping = false; let snipStartX = 0; let snipStartY = 0; let snipBox = null; console.log('[DIDI] Content script loaded on:', window.location.href); // Initialize extension function init() { console.log('[DIDI] Content script initialized'); createSelectionBox(); // Listen for messages from popup chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { console.log('[DIDI] Message received:', message); if (message.action === 'activateSelection') { currentPlatform = message.platform; console.log('[DIDI] Activating selection for platform:', currentPlatform); // Initialize platform-specific extractor if (currentPlatform === 'facebook') { extractor = new FacebookExtractor(); console.log('[DIDI] Initialized Facebook extractor'); } else if (currentPlatform === 'twitter') { extractor = new TwitterExtractor(); console.log('[DIDI] Initialized Twitter extractor'); } activateSelectionMode(); sendResponse({ success: true }); } if (message.action === 'activateSnipping') { console.log('[DIDI] Activating snipping mode'); ensureUploadConsent().then((accepted) => { if (accepted) activateSnippingMode(); sendResponse({ success: accepted }); }); return true; } if (message.action === 'activateVideoSnipping') { console.log('[DIDI] Activating video snipping mode'); ensureUploadConsent().then((accepted) => { if (accepted) activateVideoSnippingMode(); sendResponse({ success: accepted }); }); return true; } if (message.action === 'requestUploadConsent') { ensureUploadConsent().then((accepted) => sendResponse({ accepted })); return true; } if (message.action === 'showToast') { showNotification(message.message, message.type || 'info'); sendResponse({ success: true }); } if (message.action === 'showAnalysis') { console.log('[DIDI] Showing analysis result modal (pushed from background)'); showAnalysisResult(message.analysis, message.filename); sendResponse({ success: true }); } if (message.action === 'analysisFailed') { console.warn('[DIDI] Analysis failed:', message.error); showNotification('❌ Analysis failed: ' + (message.error || 'unknown'), 'error'); sendResponse({ success: true }); } return true; // Keep channel open }); } // Create selection box overlay function createSelectionBox() { console.log('[DIDI] Creating selection box'); // Remove existing box if any const existing = document.getElementById('didi-selection-box'); if (existing) { existing.remove(); } selectionBox = document.createElement('div'); selectionBox.id = 'didi-selection-box'; selectionBox.style.cssText = ` position: absolute; border: 3px solid #009198; background: rgba(0,145,152, 0.1); pointer-events: none; z-index: 999999; display: none; border-radius: 8px; box-shadow: 0 0 20px rgba(0,145,152, 0.4); `; document.body.appendChild(selectionBox); console.log('[DIDI] Selection box created'); } // Activate selection mode function activateSelectionMode() { console.log('[DIDI] Selection mode activating...'); isSelecting = true; document.body.style.cursor = 'crosshair'; // Add event listeners document.addEventListener('mousemove', handleMouseMove, true); document.addEventListener('click', handleClick, true); showNotification('✓ Selection mode active - Hover over any element and click'); console.log('[DIDI] Selection mode activated - cursor should be crosshair'); } // Deactivate selection mode function deactivateSelectionMode() { console.log('[DIDI] Selection mode deactivating...'); isSelecting = false; document.body.style.cursor = 'default'; if (selectionBox) { selectionBox.style.display = 'none'; } document.removeEventListener('mousemove', handleMouseMove, true); document.removeEventListener('click', handleClick, true); showNotification('Selection mode deactivated'); } // Handle mouse movement - highlight post containers function handleMouseMove(e) { if (!isSelecting || !extractor) return; // Find the post container using platform-specific extractor const postContainer = extractor.findPostContainer(e.target); if (!postContainer || !selectionBox) return; // Highlight the entire post container const rect = postContainer.getBoundingClientRect(); selectionBox.style.display = 'block'; selectionBox.style.top = (rect.top + window.scrollY) + 'px'; selectionBox.style.left = (rect.left + window.scrollX) + 'px'; selectionBox.style.width = rect.width + 'px'; selectionBox.style.height = rect.height + 'px'; selectedElement = postContainer; } // Handle click - extract content function handleClick(e) { console.log('[DIDI] Click event detected!'); if (!isSelecting || !extractor) { console.log('[DIDI] Not in selecting mode or no extractor'); return; } if (!selectedElement) { console.log('[DIDI] No element selected'); return; } e.preventDefault(); e.stopPropagation(); console.log('[DIDI] Extracting from element:', selectedElement); // Use platform-specific extractor const content = extractor.extract(selectedElement); console.log('[DIDI] Extracted content:', content); if (content) { // Store in chrome storage FIRST chrome.storage.local.set({ [`${currentPlatform}_content`]: content, lastExtractedContent: content, lastExtractedPlatform: currentPlatform, extractedAt: new Date().toISOString() }, () => { console.log('[DIDI] Content stored in chrome.storage'); }); // Then send to popup (if it's open) chrome.runtime.sendMessage({ action: 'contentExtracted', data: content, platform: currentPlatform }, (response) => { console.log('[DIDI] Message sent to popup, response:', response); }); // Show detailed notification — background starts the analysis automatically const summary = `✓ Postare extrasă (${content.type})\n` + `📝 Text: ${content.text ? content.text.length + ' caractere' : 'fără'}\n` + `🔗 Linkuri: ${content.links ? content.links.length : 0}\n\n` + `DiDi analizează… rezultatul apare aici în 30–90s.`; showNotification(summary, 'success'); } else { console.log('[DIDI] No content found'); showNotification('No content found', 'error'); } deactivateSelectionMode(); } // Legacy extraction function - no longer used // Platform-specific extraction is now handled by FacebookExtractor and TwitterExtractor classes // === UPLOAD CONSENT (cerință caiet: consimțământ explicit pentru fișiere/capturi) === // Shown once before the first capture/image upload; the choice is persisted. function ensureUploadConsent() { return new Promise((resolve) => { chrome.storage.local.get(['didiUploadConsent'], (data) => { if (data.didiUploadConsent === true) return resolve(true); document.querySelectorAll('.didi-consent-modal').forEach((n) => n.remove()); const overlay = document.createElement('div'); overlay.className = 'didi-consent-modal'; overlay.style.cssText = ` position: fixed; inset: 0; z-index: 10000000; background: rgba(13, 20, 36, 0.55); display: flex; align-items: center; justify-content: center; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; const card = document.createElement('div'); card.setAttribute('role', 'alertdialog'); card.setAttribute('aria-modal', 'true'); card.setAttribute('aria-label', 'Consimțământ pentru trimiterea capturii'); card.style.cssText = ` background: #fbfbfd; color: #0d1424; max-width: 420px; margin: 16px; border-radius: 22px; padding: 24px; box-shadow: 0 24px 64px rgba(13,20,36,0.35); border: 1px solid rgba(13,20,36,0.08); font-size: 14px; line-height: 1.6; `; card.innerHTML = `
Consimțământ pentru analiză
Captura sau imaginea selectată va fi trimisă platformei DiDi pentru analiză anti-dezinformare. Conținutul este folosit exclusiv pentru generarea raportului de analiză, conform politicii de confidențialitate. Trimite doar fragmentul selectat — nimic în plus.
`; overlay.appendChild(card); document.body.appendChild(overlay); const finish = (accepted) => { overlay.remove(); if (accepted) { chrome.storage.local.set({ didiUploadConsent: true, didiUploadConsentAt: new Date().toISOString() }); } resolve(accepted); }; card.querySelector('.didi-consent-accept').addEventListener('click', () => finish(true)); card.querySelector('.didi-consent-decline').addEventListener('click', () => finish(false)); overlay.addEventListener('keydown', (e) => { if (e.key === 'Escape') finish(false); }); card.querySelector('.didi-consent-accept').focus(); }); }); } // Show notification function showNotification(message, type = 'info') { console.log('[DIDI] Showing notification:', message, type); // Remove existing notifications const existing = document.querySelectorAll('.didi-notification'); existing.forEach(n => n.remove()); const notification = document.createElement('div'); notification.className = 'didi-notification'; notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 16px 24px; background: ${type === 'success' ? '#009198' : type === 'error' ? '#E63946' : '#009198'}; color: white; border-radius: 12px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 500; line-height: 1.6; z-index: 9999999; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); animation: slideIn 0.3s ease; white-space: pre-wrap; max-width: 350px; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() => notification.remove(), 300); }, 3000); } // === ANALYSIS RESULT MODAL — uses shared DidiRender module (render.js) === function showAnalysisResult(analysis, filename) { console.log('[DIDI] Showing analysis result modal'); if (!window.DidiRender || typeof window.DidiRender.renderModalHTML !== 'function') { console.error('[DIDI] DidiRender not loaded — render.js was not injected. Reload the page.'); showNotification('⚠️ Result ready but renderer missing — reload this page and check History', 'error'); return; } document.querySelectorAll('.didi-analysis-modal').forEach((n) => n.remove()); saveToLocalHistory(analysis, filename); const modal = document.createElement('div'); modal.className = 'didi-analysis-modal'; modal.innerHTML = window.DidiRender.renderModalHTML(analysis); // Inject animation keyframes once if (!document.getElementById('didi-modal-anim-styles')) { const animStyle = document.createElement('style'); animStyle.id = 'didi-modal-anim-styles'; animStyle.textContent = '@keyframes didiFadeIn{from{opacity:0}to{opacity:1}}@keyframes didiSlideUp{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}'; document.head.appendChild(animStyle); } document.body.appendChild(modal); // Wire close buttons (modal markup uses class hooks). const closeBtn = modal.querySelector('.didi-modal-close'); const gotItBtn = modal.querySelector('.didi-modal-gotit'); if (closeBtn) closeBtn.addEventListener('click', () => modal.remove()); if (gotItBtn) gotItBtn.addEventListener('click', () => modal.remove()); // Close on click outside the inner card (= click on the dark overlay). // The overlay is the modal's first child; the inner card is its grandchild. // We must compare against the actual overlay element, not "first element child" // which would also fire when clicking on the inner card's siblings. const overlay = modal.firstElementChild; // the fullscreen
with rgba(5,5,16,0.88) if (overlay) { overlay.addEventListener('click', (e) => { if (e.target === overlay) modal.remove(); // click hit the dark padding only }); } } // Save analysis to local chrome.storage history function saveToLocalHistory(analysis, filename) { chrome.storage.local.get(['didiHistory'], (data) => { const history = data.didiHistory || []; history.unshift({ analysis: analysis, filename: filename, timestamp: new Date().toISOString(), url: window.location.href }); // Keep max 50 items if (history.length > 50) history.length = 50; chrome.storage.local.set({ didiHistory: history }); }); } // ===== SNIPPING TOOL MODE ===== function activateSnippingMode() { console.log('[DIDI] Snipping mode activating...'); isSnipping = true; document.body.style.cursor = 'crosshair'; // Create snipping overlay createSnippingOverlay(); // Add event listeners document.addEventListener('mousedown', handleSnipStart, true); document.addEventListener('mousemove', handleSnipDrag, true); document.addEventListener('mouseup', handleSnipEnd, true); document.addEventListener('keydown', handleSnipCancel, true); showNotification('✂️ Snipping mode active - Click and drag to select area (ESC to cancel)', 'info'); } function createSnippingOverlay() { // Remove existing overlay const existing = document.getElementById('didi-snip-overlay'); if (existing) existing.remove(); // Create semi-transparent overlay const overlay = document.createElement('div'); overlay.id = 'didi-snip-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.3); z-index: 999998; pointer-events: none; `; document.body.appendChild(overlay); // Create snip box snipBox = document.createElement('div'); snipBox.id = 'didi-snip-box'; snipBox.style.cssText = ` position: absolute; border: 3px solid #009198; background: rgba(0,145,152, 0.15); z-index: 999999; display: none; box-shadow: 0 0 20px rgba(0,145,152, 0.6), inset 0 0 20px rgba(0,145,152, 0.1); pointer-events: none; `; document.body.appendChild(snipBox); console.log('[DIDI] Snip box created'); } function handleSnipStart(e) { if (!isSnipping) return; e.preventDefault(); e.stopPropagation(); snipStartX = e.pageX; snipStartY = e.pageY; snipBox.style.left = snipStartX + 'px'; snipBox.style.top = snipStartY + 'px'; snipBox.style.width = '0px'; snipBox.style.height = '0px'; snipBox.style.display = 'block'; } function handleSnipDrag(e) { if (!isSnipping || !snipBox || snipBox.style.display === 'none') return; e.preventDefault(); const currentX = e.pageX; const currentY = e.pageY; const width = Math.abs(currentX - snipStartX); const height = Math.abs(currentY - snipStartY); const left = Math.min(currentX, snipStartX); const top = Math.min(currentY, snipStartY); snipBox.style.width = width + 'px'; snipBox.style.height = height + 'px'; snipBox.style.left = left + 'px'; snipBox.style.top = top + 'px'; // Add size indicator inside the box snipBox.innerHTML = `
${Math.round(width)} × ${Math.round(height)}
`; } function handleSnipEnd(e) { if (!isSnipping || !snipBox || snipBox.style.display === 'none') return; e.preventDefault(); e.stopPropagation(); const rect = { left: parseInt(snipBox.style.left), top: parseInt(snipBox.style.top), width: parseInt(snipBox.style.width), height: parseInt(snipBox.style.height) }; console.log('[DIDI] Snip complete:', rect); console.log('[DIDI] Window scroll position:', { scrollX: window.scrollX, scrollY: window.scrollY }); console.log('[DIDI] Device pixel ratio:', window.devicePixelRatio); // Capture screenshot of the selected area captureSnippedArea(rect); // Deactivate snipping mode deactivateSnippingMode(); } function handleSnipCancel(e) { if (e.key === 'Escape' && isSnipping) { console.log('[DIDI] Snipping cancelled'); deactivateSnippingMode(); showNotification('Snipping cancelled', 'info'); } } function captureSnippedArea(rect) { if (rect.width < 10 || rect.height < 10) { showNotification('❌ Selection too small', 'error'); return; } showNotification('📸 Capturing screenshot...', 'info'); // Convert page coordinates to viewport coordinates // captureVisibleTab captures only the visible viewport, not the whole page const viewportRect = { left: rect.left - window.scrollX, top: rect.top - window.scrollY, width: rect.width, height: rect.height }; console.log('[DIDI] Original rect (page coords):', rect); console.log('[DIDI] Viewport rect (screen coords):', viewportRect); showNotification('📸 Capturing & analyzing… this may take 60–120s', 'info'); // Background acks immediately (success: true, accepted: true). The actual // analysis result arrives later via chrome.tabs.sendMessage → 'showAnalysis' // (see message handler at top of init()). chrome.runtime.sendMessage({ action: 'snipCaptured', rect: viewportRect, pageUrl: window.location.href, timestamp: new Date().toISOString(), devicePixelRatio: window.devicePixelRatio || 1, }, (response) => { console.log('[DIDI] Snip ack from background:', response); if (!response || !response.success) { showNotification('❌ Screenshot failed', 'error'); } }); } function downloadImageFallback(dataUrl, filename) { console.log('[DIDI] Downloading via fallback method, filename:', filename); try { // Create a temporary element const link = document.createElement('a'); link.href = dataUrl; link.download = filename || 'didi-screenshot.png'; link.style.display = 'none'; // Append to body, click, then remove document.body.appendChild(link); link.click(); // Clean up after a short delay setTimeout(() => { document.body.removeChild(link); console.log('[DIDI] ✅ Fallback download triggered successfully'); }, 100); } catch (error) { console.error('[DIDI] Fallback download failed:', error); showNotification('❌ Download failed: ' + error.message, 'error'); } } function deactivateSnippingMode() { console.log('[DIDI] Snipping mode deactivating...'); isSnipping = false; document.body.style.cursor = 'default'; // Remove overlay and box const overlay = document.getElementById('didi-snip-overlay'); if (overlay) overlay.remove(); if (snipBox) { snipBox.remove(); snipBox = null; } // Remove event listeners document.removeEventListener('mousedown', handleSnipStart, true); document.removeEventListener('mousemove', handleSnipDrag, true); document.removeEventListener('mouseup', handleSnipEnd, true); document.removeEventListener('keydown', handleSnipCancel, true); } // ===== VIDEO REGION RECORDING ===== // Uses Region Capture API (Chrome 116+) — getDisplayMedia + MediaStreamTrack.cropTo() // Records ONLY the selected rectangle, not the whole tab. let isVideoSnipping = false; let videoSnipBox = null; let videoSnipStartX = 0; let videoSnipStartY = 0; let videoMediaRecorder = null; let videoStream = null; let videoChunks = []; let videoStartTime = 0; let videoTimerInterval = null; let videoMaxDurationMs = 60000; // 60s max let savedScrollOverflow = ''; let savedHtmlOverflow = ''; function activateVideoSnippingMode() { console.log('[DIDI VIDEO] Mode activating'); if (!('CropTarget' in window) || !window.CropTarget?.fromElement) { showNotification('❌ Region Capture not supported. Update Chrome to 116+', 'error'); return; } isVideoSnipping = true; document.body.style.cursor = 'crosshair'; createVideoSnipOverlay(); document.addEventListener('mousedown', handleVideoSnipStart, true); document.addEventListener('mousemove', handleVideoSnipDrag, true); document.addEventListener('mouseup', handleVideoSnipEnd, true); document.addEventListener('keydown', handleVideoSnipCancel, true); showNotification('🎥 Video region: drag to select recording area (ESC to cancel)', 'info'); } function createVideoSnipOverlay() { const existing = document.getElementById('didi-video-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.id = 'didi-video-overlay'; overlay.style.cssText = `position:fixed; top:0; left:0; width:100vw; height:100vh; background:rgba(0,0,0,0.3); z-index:999998; pointer-events:none;`; document.body.appendChild(overlay); videoSnipBox = document.createElement('div'); videoSnipBox.id = 'didi-video-snipbox'; videoSnipBox.style.cssText = `position:absolute; border:3px solid #EF4444; background:rgba(239,68,68,0.10); z-index:999999; display:none; box-shadow:0 0 24px rgba(239,68,68,0.55), inset 0 0 20px rgba(239,68,68,0.10); pointer-events:none;`; document.body.appendChild(videoSnipBox); } function handleVideoSnipStart(e) { if (!isVideoSnipping) return; e.preventDefault(); e.stopPropagation(); videoSnipStartX = e.pageX; videoSnipStartY = e.pageY; videoSnipBox.style.left = videoSnipStartX + 'px'; videoSnipBox.style.top = videoSnipStartY + 'px'; videoSnipBox.style.width = '0px'; videoSnipBox.style.height = '0px'; videoSnipBox.style.display = 'block'; } function handleVideoSnipDrag(e) { if (!isVideoSnipping || !videoSnipBox || videoSnipBox.style.display === 'none') return; e.preventDefault(); const w = Math.abs(e.pageX - videoSnipStartX); const h = Math.abs(e.pageY - videoSnipStartY); const l = Math.min(e.pageX, videoSnipStartX); const t = Math.min(e.pageY, videoSnipStartY); videoSnipBox.style.width = w + 'px'; videoSnipBox.style.height = h + 'px'; videoSnipBox.style.left = l + 'px'; videoSnipBox.style.top = t + 'px'; videoSnipBox.innerHTML = `
${Math.round(w)} × ${Math.round(h)}
`; } function handleVideoSnipEnd(e) { if (!isVideoSnipping || !videoSnipBox || videoSnipBox.style.display === 'none') return; e.preventDefault(); e.stopPropagation(); const w = parseInt(videoSnipBox.style.width); const h = parseInt(videoSnipBox.style.height); if (w < 50 || h < 50) { showNotification('❌ Selection too small (min 50×50)', 'error'); deactivateVideoSnippingMode(); return; } // Stop selection — selection is locked, show "Start Recording" panel document.removeEventListener('mousedown', handleVideoSnipStart, true); document.removeEventListener('mousemove', handleVideoSnipDrag, true); document.removeEventListener('mouseup', handleVideoSnipEnd, true); document.body.style.cursor = 'default'; showStartRecordingPanel(); } function handleVideoSnipCancel(e) { if (e.key === 'Escape' && isVideoSnipping) { console.log('[DIDI VIDEO] Cancelled'); deactivateVideoSnippingMode(); showNotification('Cancelled', 'info'); } } function showStartRecordingPanel() { const panel = document.createElement('div'); panel.id = 'didi-video-start-panel'; panel.style.cssText = `position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:#231832; border:1px solid rgba(239,68,68,0.4); border-radius:12px; padding:14px 18px; z-index:99999999; box-shadow:0 12px 40px rgba(0,0,0,0.6); display:flex; align-items:center; gap:14px; font-family:'Inter',sans-serif;`; panel.innerHTML = `
Region selected. Ready to record?
`; document.body.appendChild(panel); panel.querySelector('#didi-start-rec-btn').addEventListener('click', async () => { const audioEnabled = panel.querySelector('#didi-audio-toggle').checked; panel.remove(); await startVideoRecording(audioEnabled); }); panel.querySelector('#didi-cancel-rec-btn').addEventListener('click', () => { panel.remove(); deactivateVideoSnippingMode(); }); } async function startVideoRecording(audioEnabled) { console.log('[DIDI VIDEO] Starting recording, audio:', audioEnabled); try { // Lock scroll to keep region anchored to viewport savedHtmlOverflow = document.documentElement.style.overflow; savedScrollOverflow = document.body.style.overflow; document.documentElement.style.overflow = 'hidden'; document.body.style.overflow = 'hidden'; // 1. Mark region for crop const cropTarget = await CropTarget.fromElement(videoSnipBox); // 2. Ask browser for tab capture videoStream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: 'browser' }, audio: audioEnabled, preferCurrentTab: true, selfBrowserSurface: 'include' }); // 3. Crop the video track to our region const [videoTrack] = videoStream.getVideoTracks(); if (typeof videoTrack.cropTo !== 'function') { throw new Error('cropTo() not available on this Chrome version'); } await videoTrack.cropTo(cropTarget); // Stop recording when user revokes share via browser bar videoTrack.addEventListener('ended', () => { console.log('[DIDI VIDEO] Track ended (user revoked sharing)'); if (videoMediaRecorder && videoMediaRecorder.state !== 'inactive') { stopVideoRecording(true); } }); // 4. Record the cropped stream const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9,opus') ? 'video/webm;codecs=vp9,opus' : 'video/webm;codecs=vp8,opus'; videoChunks = []; videoMediaRecorder = new MediaRecorder(videoStream, { mimeType, videoBitsPerSecond: 2_500_000 }); videoMediaRecorder.ondataavailable = (e) => { if (e.data && e.data.size > 0) videoChunks.push(e.data); }; videoMediaRecorder.onstop = () => onVideoRecordingStopped(mimeType); videoMediaRecorder.start(1000); // chunk every 1s videoStartTime = Date.now(); showRecordingControls(); startVideoTimer(); // Auto-stop at max duration setTimeout(() => { if (videoMediaRecorder && videoMediaRecorder.state === 'recording') { showNotification(`⏰ Max duration ${videoMaxDurationMs/1000}s reached`, 'info'); stopVideoRecording(false); } }, videoMaxDurationMs); } catch (err) { console.error('[DIDI VIDEO] Start failed:', err); showNotification('❌ ' + (err.name === 'NotAllowedError' ? 'Permission denied' : err.message), 'error'); cleanupVideoSession(); } } function showRecordingControls() { videoSnipBox.style.borderColor = '#DC2626'; videoSnipBox.style.animation = 'didiPulseRec 1.2s ease-in-out infinite'; videoSnipBox.innerHTML = ''; const controls = document.createElement('div'); controls.id = 'didi-video-controls'; controls.style.cssText = `position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:#231832; border:1px solid rgba(239,68,68,0.5); border-radius:12px; padding:12px 16px; z-index:99999999; box-shadow:0 12px 40px rgba(0,0,0,0.6); display:flex; align-items:center; gap:14px; font-family:'Inter',sans-serif;`; controls.innerHTML = `
REC 0:00 / ${Math.floor(videoMaxDurationMs/60000)}:${String(Math.floor((videoMaxDurationMs%60000)/1000)).padStart(2,'0')}
`; document.body.appendChild(controls); controls.querySelector('#didi-stop-rec-btn').addEventListener('click', () => { stopVideoRecording(false); }); } function startVideoTimer() { const timerEl = () => document.getElementById('didi-rec-timer'); videoTimerInterval = setInterval(() => { const el = timerEl(); if (!el) return; const elapsedMs = Date.now() - videoStartTime; const m = Math.floor(elapsedMs / 60000); const s = Math.floor((elapsedMs % 60000) / 1000); el.textContent = `${m}:${String(s).padStart(2, '0')}`; }, 200); } function stopVideoRecording(silentlyStopped) { if (videoTimerInterval) { clearInterval(videoTimerInterval); videoTimerInterval = null; } if (videoMediaRecorder && videoMediaRecorder.state !== 'inactive') { videoMediaRecorder.stop(); } // onstop handler will fire onVideoRecordingStopped() const ctrl = document.getElementById('didi-video-controls'); if (ctrl) ctrl.remove(); } function onVideoRecordingStopped(mimeType) { // Stop all media tracks if (videoStream) { videoStream.getTracks().forEach(t => t.stop()); videoStream = null; } const blob = new Blob(videoChunks, { type: mimeType }); videoChunks = []; console.log('[DIDI VIDEO] Recording stopped. Blob size:', blob.size, 'bytes, type:', mimeType); if (blob.size === 0) { showNotification('❌ Recording empty', 'error'); cleanupVideoSession(); return; } showSendConfirmation(blob); } function showSendConfirmation(blob) { // Restore scroll behind the modal document.documentElement.style.overflow = savedHtmlOverflow; document.body.style.overflow = savedScrollOverflow; const sizeMB = (blob.size / 1024 / 1024).toFixed(2); const durationMs = Date.now() - videoStartTime; const durSec = (durationMs / 1000).toFixed(1); // Generate preview URL for inline