@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.
- package/CLAUDE.md +18 -0
- package/README.md +226 -16
- package/chrome-extension/background.js +569 -64
- package/chrome-extension/browser-recording-manager.js +34 -0
- package/chrome-extension/content.js +438 -32
- package/chrome-extension/firebase-config.public-sw.js +1 -1
- package/chrome-extension/firebase-config.public.js +1 -1
- package/chrome-extension/frame-capture.js +31 -10
- package/chrome-extension/image-processor.js +193 -0
- package/chrome-extension/manifest.free.json +1 -1
- package/chrome-extension/options.html +2 -2
- package/chrome-extension/options.js +4 -4
- package/chrome-extension/popup.html +82 -4
- package/chrome-extension/popup.js +1106 -38
- package/chrome-extension/pro/frame-editor.html +259 -6
- package/chrome-extension/pro/frame-editor.js +959 -10
- package/chrome-extension/pro/video-exporter.js +917 -0
- package/chrome-extension/pro/video-player.js +545 -0
- package/dist/chromedebug-extension-free.zip +0 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
- package/scripts/webpack.config.free.cjs +6 -0
- package/scripts/webpack.config.pro.cjs +6 -0
- package/src/chrome-controller.js +6 -6
- package/src/database.js +226 -39
- package/src/http-server.js +55 -11
- package/src/validation/schemas.js +20 -5
|
@@ -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">×</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 =
|
|
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 =
|
|
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
|
-
|
|
1186
|
-
|
|
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 =
|
|
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
|
-
|
|
1395
|
-
|
|
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 =
|
|
2395
|
+
saveBtn.textContent = this.getDownloadButtonText();
|
|
1447
2396
|
saveBtn.disabled = false;
|
|
1448
2397
|
}
|
|
1449
2398
|
}
|