@dynamicu/chromedebug-mcp 2.7.1 → 2.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,7 @@
1
1
  // Frame Editor Logic
2
+ // Check if PRO version (used for feature gating)
3
+ const IS_PRO_VERSION = chrome.runtime.getManifest().name.includes('PRO');
4
+
2
5
  class FrameEditor {
3
6
  constructor() {
4
7
  this.sessionId = null;
@@ -8,6 +11,9 @@ class FrameEditor {
8
11
  this.lastClickedIndex = -1;
9
12
  this.isWorkflow = false; // NEW: Track if viewing workflow
10
13
  this.workflowData = null; // NEW: Store workflow data
14
+ this.videoPlayer = null; // Video player instance
15
+ this.videoExporter = null; // Video exporter instance
16
+ this.activeTab = 'frames'; // Current active tab
11
17
 
12
18
  this.init();
13
19
  }
@@ -174,6 +180,252 @@ class FrameEditor {
174
180
 
175
181
  // Setup Intersection Observer for lazy loading
176
182
  this.setupLazyLoading();
183
+
184
+ // Setup page unload cleanup
185
+ this.setupCleanup();
186
+
187
+ // Setup premium restrictions (FREE version)
188
+ this.setupPremiumRestrictions();
189
+ }
190
+
191
+ // Setup premium restrictions for FREE version
192
+ setupPremiumRestrictions() {
193
+ // Skip for PRO version
194
+ if (IS_PRO_VERSION) return;
195
+
196
+ // Inject upgrade modal into page
197
+ this.injectUpgradeModal();
198
+
199
+ // Prevent right-click on images (FREE version restriction)
200
+ document.addEventListener('contextmenu', (e) => {
201
+ // Check if right-click is on an image or within a frame preview
202
+ if (e.target.tagName === 'IMG' ||
203
+ e.target.closest('.frame-preview') ||
204
+ e.target.closest('.modal-image-container')) {
205
+ e.preventDefault();
206
+ this.showUpgradeModal('Right-click download');
207
+ }
208
+ });
209
+
210
+ // Add visual indicator to download button
211
+ const saveBtn = document.getElementById('saveEditedBtn');
212
+ if (saveBtn) {
213
+ saveBtn.innerHTML = 'Download ZIP 🔒';
214
+ saveBtn.classList.add('premium-locked');
215
+ }
216
+
217
+ // Add FREE badge to page
218
+ this.addFreeBadge();
219
+ }
220
+
221
+ // Inject the upgrade modal HTML into the page
222
+ injectUpgradeModal() {
223
+ const modalHTML = `
224
+ <div id="upgradeModal" class="upgrade-modal" style="display: none;">
225
+ <div class="upgrade-modal-content">
226
+ <button class="upgrade-modal-close" id="upgradeModalClose">&times;</button>
227
+ <h2>🔒 PRO Feature</h2>
228
+ <p id="upgradeFeatureName">This feature</p>
229
+ <p>Upgrade to Chrome Debug PRO to unlock:</p>
230
+ <ul>
231
+ <li>✅ Download individual images</li>
232
+ <li>✅ Export as ZIP archives</li>
233
+ <li>✅ Clean images (no watermark)</li>
234
+ <li>✅ Unlimited recordings</li>
235
+ <li>✅ Function tracing</li>
236
+ </ul>
237
+ <button class="upgrade-btn" id="upgradeNowBtn">Upgrade to PRO</button>
238
+ <button class="upgrade-cancel-btn" id="upgradeCancelBtn">Maybe Later</button>
239
+ </div>
240
+ </div>
241
+ `;
242
+
243
+ // Add modal styles
244
+ const styleEl = document.createElement('style');
245
+ styleEl.textContent = `
246
+ .upgrade-modal {
247
+ position: fixed;
248
+ top: 0;
249
+ left: 0;
250
+ right: 0;
251
+ bottom: 0;
252
+ background: rgba(0, 0, 0, 0.7);
253
+ display: flex;
254
+ align-items: center;
255
+ justify-content: center;
256
+ z-index: 10000;
257
+ }
258
+ .upgrade-modal-content {
259
+ background: #1e1e1e;
260
+ border-radius: 12px;
261
+ padding: 24px;
262
+ max-width: 400px;
263
+ width: 90%;
264
+ text-align: center;
265
+ position: relative;
266
+ border: 1px solid #444;
267
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
268
+ }
269
+ .upgrade-modal-close {
270
+ position: absolute;
271
+ top: 10px;
272
+ right: 12px;
273
+ background: none;
274
+ border: none;
275
+ color: #888;
276
+ font-size: 24px;
277
+ cursor: pointer;
278
+ }
279
+ .upgrade-modal-close:hover {
280
+ color: #fff;
281
+ }
282
+ .upgrade-modal-content h2 {
283
+ color: #FFD700;
284
+ margin-bottom: 12px;
285
+ font-size: 20px;
286
+ }
287
+ .upgrade-modal-content p {
288
+ color: #ccc;
289
+ margin-bottom: 10px;
290
+ font-size: 14px;
291
+ }
292
+ .upgrade-modal-content ul {
293
+ text-align: left;
294
+ list-style: none;
295
+ padding: 0;
296
+ margin: 16px 0;
297
+ }
298
+ .upgrade-modal-content li {
299
+ color: #aaa;
300
+ padding: 6px 0;
301
+ font-size: 13px;
302
+ }
303
+ .upgrade-btn {
304
+ background: linear-gradient(135deg, #FFD700, #FFA500);
305
+ color: #000;
306
+ border: none;
307
+ padding: 12px 28px;
308
+ border-radius: 6px;
309
+ font-weight: bold;
310
+ font-size: 14px;
311
+ cursor: pointer;
312
+ margin: 8px;
313
+ transition: transform 0.2s;
314
+ }
315
+ .upgrade-btn:hover {
316
+ transform: scale(1.05);
317
+ }
318
+ .upgrade-cancel-btn {
319
+ background: transparent;
320
+ color: #888;
321
+ border: 1px solid #555;
322
+ padding: 10px 24px;
323
+ border-radius: 6px;
324
+ font-size: 13px;
325
+ cursor: pointer;
326
+ margin: 8px;
327
+ }
328
+ .upgrade-cancel-btn:hover {
329
+ border-color: #888;
330
+ color: #aaa;
331
+ }
332
+ .premium-locked {
333
+ position: relative;
334
+ }
335
+ .free-badge {
336
+ position: fixed;
337
+ bottom: 10px;
338
+ left: 10px;
339
+ background: rgba(255, 152, 0, 0.9);
340
+ color: #000;
341
+ padding: 4px 10px;
342
+ border-radius: 12px;
343
+ font-size: 11px;
344
+ font-weight: bold;
345
+ z-index: 1000;
346
+ }
347
+ `;
348
+ document.head.appendChild(styleEl);
349
+
350
+ // Add modal to body
351
+ const modalDiv = document.createElement('div');
352
+ modalDiv.innerHTML = modalHTML;
353
+ document.body.appendChild(modalDiv.firstElementChild);
354
+
355
+ // Setup modal event listeners
356
+ document.getElementById('upgradeModalClose').addEventListener('click', () => {
357
+ this.hideUpgradeModal();
358
+ });
359
+
360
+ document.getElementById('upgradeCancelBtn').addEventListener('click', () => {
361
+ this.hideUpgradeModal();
362
+ });
363
+
364
+ document.getElementById('upgradeNowBtn').addEventListener('click', () => {
365
+ chrome.tabs.create({ url: 'https://chromedebug.com/checkout/buy/996773cb-682b-430f-b9e3-9ce2130bd967' });
366
+ this.hideUpgradeModal();
367
+ });
368
+
369
+ // Close on overlay click
370
+ document.getElementById('upgradeModal').addEventListener('click', (e) => {
371
+ if (e.target.id === 'upgradeModal') {
372
+ this.hideUpgradeModal();
373
+ }
374
+ });
375
+ }
376
+
377
+ // Show upgrade modal with feature name
378
+ showUpgradeModal(featureName) {
379
+ const modal = document.getElementById('upgradeModal');
380
+ const featureEl = document.getElementById('upgradeFeatureName');
381
+ if (modal && featureEl) {
382
+ featureEl.textContent = `${featureName} is a PRO feature.`;
383
+ modal.style.display = 'flex';
384
+ }
385
+ }
386
+
387
+ // Hide upgrade modal
388
+ hideUpgradeModal() {
389
+ const modal = document.getElementById('upgradeModal');
390
+ if (modal) {
391
+ modal.style.display = 'none';
392
+ }
393
+ }
394
+
395
+ // Add FREE badge indicator
396
+ addFreeBadge() {
397
+ const badge = document.createElement('div');
398
+ badge.className = 'free-badge';
399
+ badge.textContent = 'FREE VERSION';
400
+ document.body.appendChild(badge);
401
+ }
402
+
403
+ // Check if download is allowed (PRO only)
404
+ isDownloadAllowed() {
405
+ return IS_PRO_VERSION;
406
+ }
407
+
408
+ // Get the correct button text based on version (FREE shows lock icon)
409
+ getDownloadButtonText() {
410
+ return IS_PRO_VERSION ? 'Download ZIP' : 'Download ZIP 🔒';
411
+ }
412
+
413
+ // Setup cleanup handlers for memory management
414
+ setupCleanup() {
415
+ // Clean up video player when page unloads
416
+ window.addEventListener('beforeunload', () => {
417
+ if (this.videoPlayer) {
418
+ this.videoPlayer.destroy();
419
+ this.videoPlayer = null;
420
+ }
421
+ });
422
+
423
+ // Clean up when visibility changes (e.g., tab switch in browser)
424
+ document.addEventListener('visibilitychange', () => {
425
+ if (document.hidden && this.videoPlayer && this.videoPlayer.isPlaying) {
426
+ this.videoPlayer.pause();
427
+ }
428
+ });
177
429
  }
178
430
 
179
431
  async loadSession() {
@@ -215,6 +467,7 @@ class FrameEditor {
215
467
  if (response.ok) {
216
468
  const data = await response.json();
217
469
  this.frames = data.frames || [];
470
+ this.sessionInteractions = data.interactions || []; // Store session-level interactions for video player
218
471
  found = true;
219
472
  // console.log(`[INFO] Frame Editor: Loaded ${this.frames.length} frames from HTTP server on port ${port}`);
220
473
  break;
@@ -234,6 +487,7 @@ class FrameEditor {
234
487
 
235
488
  if (sessionData) {
236
489
  this.frames = sessionData.frames || [];
490
+ this.sessionInteractions = sessionData.interactions || []; // Also check storage for interactions
237
491
  // console.log(`[INFO] Frame Editor: Loaded ${this.frames.length} frames from Chrome storage (logs may not be available)`);
238
492
  } else {
239
493
  throw new Error('Session not found in HTTP server or Chrome storage');
@@ -305,9 +559,14 @@ class FrameEditor {
305
559
  // Update button text to "Download ZIP" (matches workflow behavior)
306
560
  const saveBtn = document.getElementById('saveEditedBtn');
307
561
  if (saveBtn) {
308
- saveBtn.textContent = 'Download ZIP';
562
+ saveBtn.textContent = this.getDownloadButtonText();
309
563
  saveBtn.disabled = true; // Disabled until items are selected
310
564
  }
565
+
566
+ // Show tab navigation for frame recordings (not workflows yet)
567
+ if (!this.isWorkflow && this.frames.length > 0) {
568
+ document.getElementById('editorTabs').style.display = 'flex';
569
+ }
311
570
  }
312
571
 
313
572
  // NEW: Update workflow session info
@@ -329,7 +588,7 @@ class FrameEditor {
329
588
  // Update button text for workflow mode
330
589
  const saveBtn = document.getElementById('saveEditedBtn');
331
590
  if (saveBtn) {
332
- saveBtn.textContent = 'Download ZIP';
591
+ saveBtn.textContent = this.getDownloadButtonText();
333
592
  saveBtn.disabled = true; // Disabled until items are selected
334
593
  }
335
594
 
@@ -905,7 +1164,662 @@ class FrameEditor {
905
1164
  }
906
1165
  }
907
1166
 
1167
+ // Setup tab navigation
1168
+ setupTabNavigation() {
1169
+ const tabBtns = document.querySelectorAll('.tab-btn');
1170
+ tabBtns.forEach(btn => {
1171
+ btn.addEventListener('click', () => {
1172
+ const tabId = btn.dataset.tab;
1173
+ this.switchTab(tabId);
1174
+ });
1175
+ });
1176
+ }
1177
+
1178
+ // Switch between tabs
1179
+ switchTab(tabId) {
1180
+ // Update button states
1181
+ document.querySelectorAll('.tab-btn').forEach(btn => {
1182
+ btn.classList.toggle('active', btn.dataset.tab === tabId);
1183
+ });
1184
+
1185
+ // Update tab content
1186
+ document.querySelectorAll('.tab-content').forEach(content => {
1187
+ content.classList.toggle('active', content.id === tabId + 'Tab');
1188
+ });
1189
+
1190
+ this.activeTab = tabId;
1191
+
1192
+ // Initialize video player on first switch
1193
+ if (tabId === 'player' && !this.videoPlayer && this.frames.length > 0) {
1194
+ this.initVideoPlayer();
1195
+ }
1196
+ }
1197
+
1198
+ // Initialize video player
1199
+ initVideoPlayer() {
1200
+ const container = document.getElementById('videoCanvasWrapper');
1201
+ const loading = document.getElementById('videoLoading');
1202
+
1203
+ this.videoPlayer = new VideoPlayer(container, {
1204
+ showLogs: document.getElementById('showLogsToggle').checked,
1205
+ logPosition: document.getElementById('logPositionSelect').value,
1206
+ onFrameChange: (index, frame) => this.onVideoFrameChange(index, frame),
1207
+ onPlayStateChange: (isPlaying) => this.onVideoPlayStateChange(isPlaying)
1208
+ });
1209
+
1210
+ this.videoPlayer.initialize(this.frames).then(() => {
1211
+ loading.style.display = 'none';
1212
+
1213
+ // Load interactions for cursor and click visualization
1214
+ this.loadInteractions();
1215
+
1216
+ this.setupVideoControls();
1217
+ this.setupExportControls();
1218
+
1219
+ // Initialize frame thumbnail strip
1220
+ this.initFrameStrip();
1221
+ });
1222
+ }
1223
+
1224
+ // Initialize frame thumbnail strip for visual navigation
1225
+ initFrameStrip() {
1226
+ const strip = document.getElementById('frameStrip');
1227
+ const countEl = document.getElementById('frameStripCount');
1228
+
1229
+ if (!strip || this.frames.length === 0) return;
1230
+
1231
+ // Update frame count display
1232
+ countEl.textContent = `${this.frames.length} frames`;
1233
+
1234
+ // Clear existing thumbnails
1235
+ strip.innerHTML = '';
1236
+
1237
+ // Create thumbnail for each frame
1238
+ this.frames.forEach((frame, index) => {
1239
+ const item = document.createElement('div');
1240
+ item.className = 'frame-strip-item';
1241
+ item.dataset.frameIndex = index;
1242
+
1243
+ // Create thumbnail image
1244
+ const thumb = document.createElement('img');
1245
+ thumb.className = 'frame-strip-thumb';
1246
+ thumb.alt = `Frame ${index}`;
1247
+
1248
+ // Use the frame's imageData directly (already base64)
1249
+ if (frame.imageData) {
1250
+ thumb.src = frame.imageData;
1251
+ } else {
1252
+ // Fallback placeholder for frames without images
1253
+ thumb.style.background = '#333';
1254
+ }
1255
+
1256
+ // Create info label
1257
+ const info = document.createElement('div');
1258
+ info.className = 'frame-strip-info';
1259
+ info.textContent = `${(frame.timestamp / 1000).toFixed(1)}s`;
1260
+
1261
+ item.appendChild(thumb);
1262
+ item.appendChild(info);
1263
+
1264
+ // Click handler to seek to this frame
1265
+ item.addEventListener('click', () => {
1266
+ this.videoPlayer.seek(index);
1267
+ this.updateFrameStripActiveFrame(index);
1268
+ });
1269
+
1270
+ strip.appendChild(item);
1271
+ });
1272
+
1273
+ // Set first frame as active
1274
+ this.updateFrameStripActiveFrame(0);
1275
+ }
1276
+
1277
+ // Update active frame highlighting in thumbnail strip
1278
+ updateFrameStripActiveFrame(activeIndex) {
1279
+ const strip = document.getElementById('frameStrip');
1280
+ if (!strip) return;
1281
+
1282
+ // Remove active class from all items
1283
+ strip.querySelectorAll('.frame-strip-item').forEach(item => {
1284
+ item.classList.remove('active');
1285
+ });
1286
+
1287
+ // Add active class to current frame
1288
+ const activeItem = strip.querySelector(`.frame-strip-item[data-frame-index="${activeIndex}"]`);
1289
+ if (activeItem) {
1290
+ activeItem.classList.add('active');
1291
+
1292
+ // Scroll to keep active frame visible
1293
+ const stripRect = strip.getBoundingClientRect();
1294
+ const itemRect = activeItem.getBoundingClientRect();
1295
+
1296
+ // Check if item is outside visible area
1297
+ if (itemRect.left < stripRect.left || itemRect.right > stripRect.right) {
1298
+ activeItem.scrollIntoView({
1299
+ behavior: 'smooth',
1300
+ block: 'nearest',
1301
+ inline: 'center'
1302
+ });
1303
+ }
1304
+ }
1305
+ }
1306
+
1307
+ // Setup export controls
1308
+ setupExportControls() {
1309
+ const exportBtn = document.getElementById('exportVideoBtn');
1310
+ const modal = document.getElementById('exportModal');
1311
+ const closeBtn = document.getElementById('closeExportModal');
1312
+ const cancelBtn = document.getElementById('cancelExportBtn');
1313
+ const startBtn = document.getElementById('startExportBtn');
1314
+
1315
+ // Disable export for FREE version
1316
+ if (!IS_PRO_VERSION && exportBtn) {
1317
+ exportBtn.disabled = true;
1318
+ exportBtn.style.opacity = '0.5';
1319
+ exportBtn.style.cursor = 'not-allowed';
1320
+ exportBtn.title = 'MP4 Export is a PRO feature. Upgrade to export videos.';
1321
+
1322
+ // Add PRO badge to button
1323
+ const proBadge = document.createElement('span');
1324
+ proBadge.textContent = ' PRO';
1325
+ proBadge.style.cssText = 'background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 2px 6px; border-radius: 4px; font-size: 9px; margin-left: 6px; font-weight: bold;';
1326
+ exportBtn.appendChild(proBadge);
1327
+ }
1328
+
1329
+ // Open modal (only for PRO users)
1330
+ exportBtn?.addEventListener('click', () => {
1331
+ // Block export for FREE users
1332
+ if (!IS_PRO_VERSION) {
1333
+ this.showUpgradeModal('MP4 Video Export');
1334
+ return;
1335
+ }
1336
+
1337
+ if (!VideoExporter.isSupported()) {
1338
+ alert('Video export requires a modern browser with WebCodecs support (Chrome 94+)');
1339
+ return;
1340
+ }
1341
+ this.updateExportEstimates();
1342
+ modal.style.display = 'flex';
1343
+ });
1344
+
1345
+ // Close modal
1346
+ closeBtn?.addEventListener('click', () => modal.style.display = 'none');
1347
+ cancelBtn?.addEventListener('click', () => {
1348
+ if (this.videoExporter) {
1349
+ this.videoExporter.cancel();
1350
+ }
1351
+ modal.style.display = 'none';
1352
+ });
1353
+
1354
+ // Click outside to close
1355
+ modal?.addEventListener('click', (e) => {
1356
+ if (e.target === modal) modal.style.display = 'none';
1357
+ });
1358
+
1359
+ // Update estimates when FPS changes
1360
+ const fpsSelect = document.getElementById('exportFps');
1361
+ fpsSelect?.addEventListener('change', () => this.updateExportEstimates());
1362
+
1363
+ // Update estimates when intro/outro text changes
1364
+ document.getElementById('exportIntroText')?.addEventListener('input', () => this.updateExportEstimates());
1365
+ document.getElementById('exportOutroText')?.addEventListener('input', () => this.updateExportEstimates());
1366
+ document.getElementById('exportIntroDuration')?.addEventListener('change', () => this.updateExportEstimates());
1367
+ document.getElementById('exportOutroDuration')?.addEventListener('change', () => this.updateExportEstimates());
1368
+
1369
+ // Start export
1370
+ startBtn?.addEventListener('click', () => this.startExport());
1371
+ }
1372
+
1373
+ // Update export duration and size estimates
1374
+ updateExportEstimates() {
1375
+ // Calculate base recording duration
1376
+ const recordingDuration = this.frames.length > 1
1377
+ ? (this.frames[this.frames.length - 1].timestamp - this.frames[0].timestamp) / 1000
1378
+ : 0;
1379
+
1380
+ // Add intro/outro duration
1381
+ const introText = document.getElementById('exportIntroText')?.value.trim() || '';
1382
+ const outroText = document.getElementById('exportOutroText')?.value.trim() || '';
1383
+ const introDuration = introText ? parseInt(document.getElementById('exportIntroDuration')?.value || 2) : 0;
1384
+ const outroDuration = outroText ? parseInt(document.getElementById('exportOutroDuration')?.value || 2) : 0;
1385
+
1386
+ const totalDuration = recordingDuration + introDuration + outroDuration;
1387
+
1388
+ // Calculate frame count
1389
+ const fps = parseInt(document.getElementById('exportFps')?.value || 10);
1390
+ const introFrames = introText ? Math.ceil(introDuration * fps) : 0;
1391
+ const outroFrames = outroText ? Math.ceil(outroDuration * fps) : 0;
1392
+ const totalFrames = this.frames.length + introFrames + outroFrames;
1393
+
1394
+ // Estimate file size (rough estimate: bitrate * duration)
1395
+ const quality = document.getElementById('exportQuality')?.value || 'medium';
1396
+ const bitrates = { low: 1, medium: 3, high: 5 }; // Mbps
1397
+ const estimatedSizeMB = (bitrates[quality] * totalDuration) / 8;
1398
+
1399
+ // Update or create estimate display
1400
+ let estimateDiv = document.getElementById('exportEstimate');
1401
+ if (!estimateDiv) {
1402
+ estimateDiv = document.createElement('div');
1403
+ estimateDiv.id = 'exportEstimate';
1404
+ estimateDiv.className = 'export-estimate';
1405
+ const footer = document.querySelector('.export-modal-footer');
1406
+ footer?.parentNode.insertBefore(estimateDiv, footer);
1407
+ }
1408
+
1409
+ // Show warning for long recordings
1410
+ const warningClass = totalDuration > 120 ? 'export-warning' : '';
1411
+ const warningText = totalDuration > 120 ? '<span class="warning-text">Large exports may take several minutes</span>' : '';
1412
+
1413
+ estimateDiv.innerHTML = `
1414
+ <div class="estimate-row ${warningClass}">
1415
+ <span>Duration: ~${Math.round(totalDuration)}s</span>
1416
+ <span>Frames: ${totalFrames}</span>
1417
+ <span>Est. Size: ~${estimatedSizeMB.toFixed(1)} MB</span>
1418
+ </div>
1419
+ ${warningText}
1420
+ `;
1421
+ }
1422
+
1423
+ async startExport() {
1424
+ // Check if export is allowed (FREE version restriction)
1425
+ if (!this.isDownloadAllowed()) {
1426
+ this.showUpgradeModal('Video Export');
1427
+ return;
1428
+ }
1429
+
1430
+ // DEBUG: Log frame data before export
1431
+ console.log('[FrameEditor] startExport called:', {
1432
+ framesCount: this.frames?.length || 0,
1433
+ sessionId: this.sessionId,
1434
+ isWorkflow: this.isWorkflow
1435
+ });
1436
+
1437
+ if (this.frames && this.frames.length > 0) {
1438
+ const firstFrame = this.frames[0];
1439
+ console.log('[FrameEditor] First frame structure:', {
1440
+ keys: Object.keys(firstFrame),
1441
+ hasImageData: !!firstFrame.imageData,
1442
+ imageDataType: typeof firstFrame.imageData,
1443
+ imageDataLength: firstFrame.imageData?.length || 0,
1444
+ imageDataPrefix: firstFrame.imageData?.substring?.(0, 80) || 'N/A',
1445
+ timestamp: firstFrame.timestamp
1446
+ });
1447
+ } else {
1448
+ console.error('[FrameEditor] NO FRAMES AVAILABLE FOR EXPORT!');
1449
+ }
1450
+
1451
+ const progressDiv = document.getElementById('exportProgress');
1452
+ const progressBar = document.getElementById('exportProgressBar');
1453
+ const progressText = document.getElementById('exportProgressText');
1454
+ const startBtn = document.getElementById('startExportBtn');
1455
+
1456
+ // Get basic options
1457
+ const options = {
1458
+ quality: document.getElementById('exportQuality').value,
1459
+ fps: parseInt(document.getElementById('exportFps').value),
1460
+ includeLogs: document.getElementById('exportIncludeLogs').checked,
1461
+ includeClicks: document.getElementById('exportIncludeClicks').checked,
1462
+ includeCursor: document.getElementById('exportIncludeCursor').checked,
1463
+ logPosition: document.getElementById('exportLogPosition').value,
1464
+ logFilter: document.getElementById('exportLogFilter').value,
1465
+
1466
+ // Style preset
1467
+ stylePreset: document.getElementById('exportStylePreset').value,
1468
+
1469
+ // Intro/Outro options
1470
+ introText: document.getElementById('exportIntroText').value.trim(),
1471
+ outroText: document.getElementById('exportOutroText').value.trim(),
1472
+ introDuration: parseInt(document.getElementById('exportIntroDuration').value),
1473
+ outroDuration: parseInt(document.getElementById('exportOutroDuration').value),
1474
+
1475
+ // Watermark options
1476
+ watermarkText: document.getElementById('exportWatermarkText').value.trim(),
1477
+ watermarkPosition: document.getElementById('exportWatermarkPosition').value,
1478
+ watermarkOpacity: parseFloat(document.getElementById('exportWatermarkOpacity').value),
1479
+
1480
+ onProgress: (percent, current, total) => {
1481
+ progressBar.style.width = percent + '%';
1482
+ progressText.textContent = `${Math.round(percent)}% (${current}/${total} frames)`;
1483
+ },
1484
+ onComplete: (blob, filename) => {
1485
+ VideoExporter.downloadBlob(blob, filename);
1486
+ progressDiv.style.display = 'none';
1487
+ startBtn.disabled = false;
1488
+ startBtn.textContent = 'Export MP4';
1489
+ document.getElementById('exportModal').style.display = 'none';
1490
+ this.showToast('Video exported successfully!');
1491
+ },
1492
+ onError: (error) => {
1493
+ alert('Export failed: ' + error.message);
1494
+ progressDiv.style.display = 'none';
1495
+ startBtn.disabled = false;
1496
+ startBtn.textContent = 'Export MP4';
1497
+ }
1498
+ };
1499
+
1500
+ // Show progress
1501
+ progressDiv.style.display = 'block';
1502
+ progressBar.style.width = '0%';
1503
+ progressText.textContent = '0%';
1504
+ startBtn.disabled = true;
1505
+ startBtn.textContent = 'Exporting...';
1506
+
1507
+ // Create exporter and start
1508
+ this.videoExporter = new VideoExporter(options);
1509
+ const interactions = await this.loadInteractions();
1510
+ await this.videoExporter.exportVideo(this.frames, interactions, this.sessionId);
1511
+ }
1512
+
1513
+ // Setup video player controls
1514
+ setupVideoControls() {
1515
+ // Play/Pause
1516
+ const playBtn = document.getElementById('playPauseBtn');
1517
+ playBtn.addEventListener('click', () => {
1518
+ if (this.videoPlayer.isPlaying) {
1519
+ this.videoPlayer.pause();
1520
+ } else {
1521
+ this.videoPlayer.play();
1522
+ }
1523
+ });
1524
+
1525
+ // Frame navigation
1526
+ document.getElementById('prevFrameBtn').addEventListener('click', () => {
1527
+ this.videoPlayer.prevFrame();
1528
+ });
1529
+
1530
+ document.getElementById('nextFrameBtn').addEventListener('click', () => {
1531
+ this.videoPlayer.nextFrame();
1532
+ });
1533
+
1534
+ // Seek bar
1535
+ const seekBar = document.getElementById('seekBar');
1536
+ seekBar.addEventListener('input', (e) => {
1537
+ const frameIndex = Math.round((e.target.value / 100) * (this.frames.length - 1));
1538
+ this.videoPlayer.seek(frameIndex);
1539
+ });
1540
+
1541
+ // Speed
1542
+ document.getElementById('speedSelect').addEventListener('change', (e) => {
1543
+ this.videoPlayer.setSpeed(parseFloat(e.target.value));
1544
+ });
1545
+
1546
+ // Cursor and click toggles
1547
+ document.getElementById('showCursorToggle').addEventListener('change', (e) => {
1548
+ this.videoPlayer.setShowMouseCursor(e.target.checked);
1549
+ });
1550
+
1551
+ document.getElementById('showClicksToggle').addEventListener('change', (e) => {
1552
+ this.videoPlayer.setShowClickIndicators(e.target.checked);
1553
+ });
1554
+
1555
+ // Setup log panel
1556
+ this.setupLogPanel();
1557
+
1558
+ // Keyboard shortcuts
1559
+ document.addEventListener('keydown', (e) => {
1560
+ if (this.activeTab !== 'player') return;
1561
+
1562
+ // Skip keyboard shortcuts when user is typing in input fields
1563
+ if (!this.shouldHandleKeyboardShortcut(e)) return;
1564
+
1565
+ switch(e.code) {
1566
+ case 'Space':
1567
+ e.preventDefault();
1568
+ if (this.videoPlayer.isPlaying) {
1569
+ this.videoPlayer.pause();
1570
+ } else {
1571
+ this.videoPlayer.play();
1572
+ }
1573
+ break;
1574
+ case 'ArrowLeft':
1575
+ e.preventDefault();
1576
+ this.videoPlayer.prevFrame();
1577
+ break;
1578
+ case 'ArrowRight':
1579
+ e.preventDefault();
1580
+ this.videoPlayer.nextFrame();
1581
+ break;
1582
+ case 'Home':
1583
+ e.preventDefault();
1584
+ this.videoPlayer.seek(0);
1585
+ break;
1586
+ case 'End':
1587
+ e.preventDefault();
1588
+ this.videoPlayer.seek(this.frames.length - 1);
1589
+ break;
1590
+ case 'KeyL':
1591
+ e.preventDefault();
1592
+ const logToggle = document.getElementById('showLogsToggle');
1593
+ logToggle.checked = !logToggle.checked;
1594
+ logToggle.dispatchEvent(new Event('change'));
1595
+ break;
1596
+ case 'Digit1':
1597
+ this.videoPlayer.setSpeed(0.5);
1598
+ document.getElementById('speedSelect').value = '0.5';
1599
+ break;
1600
+ case 'Digit2':
1601
+ this.videoPlayer.setSpeed(1);
1602
+ document.getElementById('speedSelect').value = '1';
1603
+ break;
1604
+ case 'Digit3':
1605
+ this.videoPlayer.setSpeed(2);
1606
+ document.getElementById('speedSelect').value = '2';
1607
+ break;
1608
+ case 'Digit4':
1609
+ this.videoPlayer.setSpeed(4);
1610
+ document.getElementById('speedSelect').value = '4';
1611
+ break;
1612
+ case 'KeyE':
1613
+ e.preventDefault();
1614
+ const exportBtn = document.getElementById('exportVideoBtn');
1615
+ if (exportBtn) exportBtn.click();
1616
+ break;
1617
+ }
1618
+ });
1619
+ }
1620
+
1621
+ // Helper to check if keyboard shortcut should be handled
1622
+ // Returns false if user is typing in an input field
1623
+ shouldHandleKeyboardShortcut(event) {
1624
+ const target = event.target;
1625
+ const tagName = target.tagName.toUpperCase();
1626
+
1627
+ // Standard form elements that handle their own keyboard
1628
+ const formElements = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'];
1629
+ if (formElements.includes(tagName)) return false;
1630
+
1631
+ // Content editable elements
1632
+ if (target.isContentEditable) return false;
1633
+
1634
+ // Elements with explicit interaction roles
1635
+ const role = target.getAttribute('role');
1636
+ const interactiveRoles = ['button', 'textbox', 'listbox', 'combobox'];
1637
+ if (role && interactiveRoles.includes(role)) return false;
1638
+
1639
+ return true;
1640
+ }
1641
+
1642
+ // Handle video frame change
1643
+ onVideoFrameChange(index, frame) {
1644
+ // Update UI
1645
+ document.getElementById('seekBar').value = (index / (this.frames.length - 1)) * 100;
1646
+ document.getElementById('frameCounter').textContent = `Frame ${index + 1} / ${this.frames.length}`;
1647
+
1648
+ // Update time display
1649
+ const currentTime = this.formatTime(this.videoPlayer.getCurrentTime());
1650
+ const duration = this.formatTime(this.videoPlayer.getDuration());
1651
+ document.getElementById('timeDisplay').textContent = `${currentTime} / ${duration}`;
1652
+
1653
+ // Update log panel if visible
1654
+ this.updateLogPanel(frame);
1655
+
1656
+ // Update thumbnail strip active frame
1657
+ this.updateFrameStripActiveFrame(index);
1658
+ }
1659
+
1660
+ // Handle video play state change
1661
+ onVideoPlayStateChange(isPlaying) {
1662
+ const btn = document.getElementById('playPauseBtn');
1663
+ btn.textContent = isPlaying ? '⏸' : '▶';
1664
+ btn.classList.toggle('playing', isPlaying);
1665
+ }
1666
+
1667
+ // Format time in milliseconds to MM:SS
1668
+ formatTime(ms) {
1669
+ const seconds = Math.floor(ms / 1000);
1670
+ const minutes = Math.floor(seconds / 60);
1671
+ const secs = seconds % 60;
1672
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
1673
+ }
1674
+
1675
+ // Setup log panel controls
1676
+ setupLogPanel() {
1677
+ const logToggle = document.getElementById('showLogsToggle');
1678
+ const logPositionSelect = document.getElementById('logPositionSelect');
1679
+ const logPanel = document.getElementById('videoLogPanel');
1680
+ const logPanelClose = document.getElementById('logPanelClose');
1681
+
1682
+ // Show/hide log panel
1683
+ logToggle.addEventListener('change', (e) => {
1684
+ if (e.target.checked) {
1685
+ logPanel.style.display = 'flex';
1686
+ this.updateLogPanel(this.frames[this.videoPlayer.currentFrameIndex]);
1687
+ } else {
1688
+ logPanel.style.display = 'none';
1689
+ }
1690
+ });
1691
+
1692
+ // Change log position
1693
+ logPositionSelect.addEventListener('change', (e) => {
1694
+ this.setLogPosition(e.target.value);
1695
+ });
1696
+
1697
+ // Close button
1698
+ logPanelClose.addEventListener('click', () => {
1699
+ logToggle.checked = false;
1700
+ logPanel.style.display = 'none';
1701
+ });
1702
+
1703
+ // Initialize position
1704
+ this.setLogPosition(logPositionSelect.value);
1705
+ }
1706
+
1707
+ // Set log panel position
1708
+ setLogPosition(position) {
1709
+ const logPanel = document.getElementById('videoLogPanel');
1710
+
1711
+ // Remove all position classes
1712
+ logPanel.classList.remove('position-bottom', 'position-right', 'position-overlay');
1713
+
1714
+ // Add new position class
1715
+ if (position !== 'hidden') {
1716
+ logPanel.classList.add(`position-${position}`);
1717
+ }
1718
+ }
1719
+
1720
+ // Update log panel with current frame's logs
1721
+ updateLogPanel(frame) {
1722
+ const logPanel = document.getElementById('videoLogPanel');
1723
+ const logContent = document.getElementById('logPanelContent');
1724
+
1725
+ if (!frame || !logPanel || logPanel.style.display === 'none') return;
1726
+
1727
+ // Get logs for this frame
1728
+ const logs = frame.logs || [];
1729
+
1730
+ if (logs.length === 0) {
1731
+ logContent.innerHTML = '<div style="color: #666; padding: 20px; text-align: center;">No console logs for this frame</div>';
1732
+ return;
1733
+ }
1734
+
1735
+ // Render logs
1736
+ logContent.innerHTML = logs.map(log => {
1737
+ const level = log.level || 'log';
1738
+ const relativeTime = log.relativeTime || log.relative_time || 0;
1739
+ const timeStr = `${(relativeTime / 1000).toFixed(3)}s`;
1740
+ const message = this.escapeHtml(log.message || '');
1741
+
1742
+ return `
1743
+ <div class="log-entry">
1744
+ <span class="log-level ${level}">${level}</span>
1745
+ <span class="log-time">${timeStr}</span>
1746
+ <span class="log-message">${message}</span>
1747
+ </div>
1748
+ `;
1749
+ }).join('');
1750
+ }
1751
+
1752
+ // Escape HTML to prevent XSS
1753
+ escapeHtml(text) {
1754
+ const div = document.createElement('div');
1755
+ div.textContent = text;
1756
+ return div.innerHTML;
1757
+ }
1758
+
1759
+ // Load interactions from frames and session-level data
1760
+ loadInteractions() {
1761
+ // Extract interactions from session-level data and frame associations
1762
+ // Interactions include clicks, inputs, scrolls, and mouse movements
1763
+ const interactions = [];
1764
+
1765
+ // First, add session-level interactions (from API response)
1766
+ if (this.sessionInteractions && this.sessionInteractions.length > 0) {
1767
+ interactions.push(...this.sessionInteractions);
1768
+ }
1769
+
1770
+ // Then check each frame for associated interactions (already timestamp-aligned by database)
1771
+ this.frames.forEach((frame) => {
1772
+ if (frame.associatedInteractions && Array.isArray(frame.associatedInteractions)) {
1773
+ interactions.push(...frame.associatedInteractions);
1774
+ }
1775
+
1776
+ // Legacy format: check for embedded interactions array
1777
+ if (frame.interactions && Array.isArray(frame.interactions)) {
1778
+ interactions.push(...frame.interactions);
1779
+ }
1780
+
1781
+ // Legacy format: check for click/mousemove fields directly on frame
1782
+ if (frame.click && frame.click.x !== undefined) {
1783
+ interactions.push({
1784
+ type: 'click',
1785
+ x: frame.click.x,
1786
+ y: frame.click.y,
1787
+ timestamp: frame.timestamp
1788
+ });
1789
+ }
1790
+
1791
+ if (frame.mousePosition && frame.mousePosition.x !== undefined) {
1792
+ interactions.push({
1793
+ type: 'mousemove',
1794
+ x: frame.mousePosition.x,
1795
+ y: frame.mousePosition.y,
1796
+ timestamp: frame.timestamp
1797
+ });
1798
+ }
1799
+ });
1800
+
1801
+ // Deduplicate by timestamp + type + coordinates to prevent double-rendering
1802
+ const seen = new Set();
1803
+ const deduped = interactions.filter(interaction => {
1804
+ const key = `${interaction.type}-${interaction.timestamp}-${interaction.x}-${interaction.y}`;
1805
+ if (seen.has(key)) return false;
1806
+ seen.add(key);
1807
+ return true;
1808
+ });
1809
+
1810
+ // Load into video player
1811
+ if (this.videoPlayer) {
1812
+ this.videoPlayer.loadInteractions(deduped);
1813
+ }
1814
+
1815
+ return deduped;
1816
+ }
1817
+
908
1818
  setupEventListeners() {
1819
+ // Setup tab navigation
1820
+ this.setupTabNavigation();
1821
+
1822
+
909
1823
  // Copy ID button
910
1824
  document.getElementById('copyIdBtn').addEventListener('click', () => {
911
1825
  if (this.sessionId) {
@@ -1023,6 +1937,12 @@ class FrameEditor {
1023
1937
  }
1024
1938
 
1025
1939
  async saveEditedRecording() {
1940
+ // Check if download is allowed (FREE version restriction)
1941
+ if (!this.isDownloadAllowed()) {
1942
+ this.showUpgradeModal('ZIP Download');
1943
+ return;
1944
+ }
1945
+
1026
1946
  // Both frame and workflow recordings now use ZIP download
1027
1947
  if (this.isWorkflow) {
1028
1948
  this.downloadWorkflowAsZip();
@@ -1181,11 +2101,25 @@ class FrameEditor {
1181
2101
  zip.file('README.txt', readme);
1182
2102
 
1183
2103
  // Extract and add screenshots using ORIGINAL action indices
2104
+ // Apply watermark for FREE version at export time (not storage time)
1184
2105
  let screenshotCount = 0;
1185
- actionsToExport.forEach(({ action, originalIndex }) => {
1186
- const screenshotData = action.screenshot || action.screenshot_data;
2106
+ const imageProcessor = window.ChromeDebugImageProcessor;
2107
+
2108
+ // Process screenshots with potential watermarking
2109
+ await Promise.all(actionsToExport.map(async ({ action, originalIndex }) => {
2110
+ let screenshotData = action.screenshot || action.screenshot_data;
1187
2111
  if (!screenshotData) return;
1188
2112
 
2113
+ // Apply watermark at EXPORT time for FREE version
2114
+ if (imageProcessor && imageProcessor.shouldWatermark()) {
2115
+ try {
2116
+ screenshotData = await imageProcessor.processImageForExport(screenshotData);
2117
+ } catch (err) {
2118
+ console.warn('[FrameEditor] Failed to apply watermark:', err);
2119
+ // Continue with original image if watermarking fails
2120
+ }
2121
+ }
2122
+
1189
2123
  const paddedIndex = String(originalIndex).padStart(3, '0');
1190
2124
  // Handle both data URL format and raw base64
1191
2125
  let imageData = screenshotData;
@@ -1203,7 +2137,7 @@ class FrameEditor {
1203
2137
  const filename = `screenshots/action_${paddedIndex}.${extension}`;
1204
2138
  zip.file(filename, imageData, { base64: true });
1205
2139
  screenshotCount++;
1206
- });
2140
+ }));
1207
2141
 
1208
2142
  // Generate ZIP file
1209
2143
  const blob = await zip.generateAsync({
@@ -1234,7 +2168,7 @@ class FrameEditor {
1234
2168
  alert('Failed to create ZIP file: ' + error.message);
1235
2169
 
1236
2170
  const saveBtn = document.getElementById('saveEditedBtn');
1237
- saveBtn.textContent = 'Download ZIP';
2171
+ saveBtn.textContent = this.getDownloadButtonText();
1238
2172
  saveBtn.disabled = false;
1239
2173
  }
1240
2174
  }
@@ -1390,11 +2324,26 @@ https://github.com/anthropics/chrome-debug
1390
2324
  zip.file('README.txt', readme);
1391
2325
 
1392
2326
  // Extract and add screenshots using ORIGINAL frame indices
2327
+ // Apply watermark for FREE version at export time (not storage time)
1393
2328
  let screenshotCount = 0;
1394
- framesToExport.forEach(({ frame, originalIndex }) => {
1395
- const imageData = frame.imageData;
2329
+ const imageProcessorFrames = window.ChromeDebugImageProcessor;
2330
+
2331
+ // Process frames with potential watermarking
2332
+ await Promise.all(framesToExport.map(async ({ frame, originalIndex }) => {
2333
+ let imageData = frame.imageData;
1396
2334
  if (!imageData) return;
1397
2335
 
2336
+ // Apply watermark at EXPORT time for FREE version
2337
+ if (imageProcessorFrames && imageProcessorFrames.shouldWatermark()) {
2338
+ try {
2339
+ imageData = await imageProcessorFrames.processImageForExport(imageData);
2340
+ } catch (err) {
2341
+ console.warn('[FrameEditor] Failed to apply watermark to frame:', err);
2342
+ // Continue with original image if watermarking fails
2343
+ imageData = frame.imageData;
2344
+ }
2345
+ }
2346
+
1398
2347
  const paddedIndex = String(originalIndex).padStart(3, '0');
1399
2348
 
1400
2349
  // Handle both data URL format and raw base64
@@ -1412,7 +2361,7 @@ https://github.com/anthropics/chrome-debug
1412
2361
  const filename = `screenshots/frame_${paddedIndex}.${extension}`;
1413
2362
  zip.file(filename, base64Data, { base64: true });
1414
2363
  screenshotCount++;
1415
- });
2364
+ }));
1416
2365
 
1417
2366
  // Generate ZIP file
1418
2367
  const blob = await zip.generateAsync({
@@ -1443,7 +2392,7 @@ https://github.com/anthropics/chrome-debug
1443
2392
  alert('Failed to create ZIP file: ' + error.message);
1444
2393
 
1445
2394
  const saveBtn = document.getElementById('saveEditedBtn');
1446
- saveBtn.textContent = 'Download ZIP';
2395
+ saveBtn.textContent = this.getDownloadButtonText();
1447
2396
  saveBtn.disabled = false;
1448
2397
  }
1449
2398
  }