Restore three-button capture interface: Share Entire Screen, Snip, Cancel

This commit is contained in:
2026-01-11 05:14:15 +00:00
parent ccb239c84b
commit 54d262b97d
6 changed files with 1125 additions and 15 deletions
+348 -14
View File
@@ -17,6 +17,235 @@
}
}
</script>
<!-- Import standalone tools -->
<script>
// Initialize capture tools globally
console.log('Initializing capture tools...');
window.captureScreenshot = async function() {
console.log('captureScreenshot called');
try {
const mediaStream = await navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' }
});
const video = document.createElement('video');
video.srcObject = mediaStream;
return new Promise((resolve) => {
video.onloadedmetadata = () => {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, 0, 0);
mediaStream.getTracks().forEach(track => track.stop());
resolve(canvas);
};
video.play();
});
} catch (error) {
console.error('Screenshot capture failed:', error);
return null;
}
};
window.captureSnip = async function() {
console.log('captureSnip called');
return new Promise((resolve) => {
navigator.mediaDevices.getDisplayMedia({
video: { mediaSource: 'screen' }
}).then(mediaStream => {
const video = document.createElement('video');
video.srcObject = mediaStream;
video.onloadedmetadata = () => {
const fullCanvas = document.createElement('canvas');
fullCanvas.width = video.videoWidth;
fullCanvas.height = video.videoHeight;
const fullCtx = fullCanvas.getContext('2d');
fullCtx.drawImage(video, 0, 0);
mediaStream.getTracks().forEach(track => track.stop());
// Create fullscreen preview container
const container = document.createElement('div');
container.style.cssText = `position:fixed;top:0;left:0;width:100%;height:100%;background:black;z-index:99999;overflow:hidden;`;
// Create image preview of the captured screen
const previewImg = document.createElement('img');
previewImg.src = fullCanvas.toDataURL();
previewImg.style.cssText = `width:100%;height:100%;object-fit:contain;`;
// Create overlay canvas for drawing selection
const overlayCanvas = document.createElement('canvas');
overlayCanvas.style.cssText = `position:absolute;top:0;left:0;cursor:crosshair;display:none;`;
// Create control panel
const controlPanel = document.createElement('div');
controlPanel.style.cssText = `position:absolute;bottom:30px;left:50%;transform:translateX(-50%);display:flex;gap:15px;z-index:100001;`;
const fullScreenBtn = document.createElement('button');
fullScreenBtn.textContent = 'Share Entire Screen';
fullScreenBtn.style.cssText = `padding:12px 24px;background:#10B981;color:white;border:none;border-radius:6px;font-size:16px;font-weight:bold;cursor:pointer;`;
const snippetBtn = document.createElement('button');
snippetBtn.textContent = 'Snip';
snippetBtn.style.cssText = `padding:12px 24px;background:#0EA5E9;color:white;border:none;border-radius:6px;font-size:16px;font-weight:bold;cursor:pointer;`;
const cancelBtn = document.createElement('button');
cancelBtn.textContent = '✕ Cancel';
cancelBtn.style.cssText = `padding:12px 24px;background:#EF4444;color:white;border:none;border-radius:6px;font-size:16px;font-weight:bold;cursor:pointer;`;
controlPanel.appendChild(fullScreenBtn);
controlPanel.appendChild(snippetBtn);
controlPanel.appendChild(cancelBtn);
container.appendChild(previewImg);
container.appendChild(overlayCanvas);
container.appendChild(controlPanel);
document.body.appendChild(container);
let snippingMode = false;
// Wait for image to load
previewImg.onload = () => {
const rect = previewImg.getBoundingClientRect();
overlayCanvas.width = rect.width;
overlayCanvas.height = rect.height;
const displayWidth = fullCanvas.width;
const displayHeight = fullCanvas.height;
const scaleX = displayWidth / rect.width;
const scaleY = displayHeight / rect.height;
let isDrawing = false, startX = 0, startY = 0, endX = 0, endY = 0;
const ctx = overlayCanvas.getContext('2d');
function drawSelection() {
ctx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
if (!isDrawing && startX === endX && startY === endY) return;
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
const w = Math.abs(endX - startX);
const h = Math.abs(endY - startY);
ctx.strokeStyle = '#0EA5E9';
ctx.lineWidth = 3;
ctx.strokeRect(x, y, w, h);
ctx.fillStyle = 'rgba(14, 165, 233, 0.1)';
ctx.fillRect(x, y, w, h);
const sz = 12;
ctx.fillStyle = '#0EA5E9';
ctx.fillRect(x - sz/2, y - sz/2, sz, sz);
ctx.fillRect(x + w - sz/2, y - sz/2, sz, sz);
ctx.fillRect(x - sz/2, y + h - sz/2, sz, sz);
ctx.fillRect(x + w - sz/2, y + h - sz/2, sz, sz);
}
overlayCanvas.addEventListener('mousedown', (e) => {
if (!snippingMode) return;
const rect = overlayCanvas.getBoundingClientRect();
isDrawing = true;
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
endX = startX;
endY = startY;
});
overlayCanvas.addEventListener('mousemove', (e) => {
if (!snippingMode || !isDrawing) return;
const rect = overlayCanvas.getBoundingClientRect();
endX = e.clientX - rect.left;
endY = e.clientY - rect.top;
drawSelection();
});
overlayCanvas.addEventListener('mouseup', () => {
if (!snippingMode || !isDrawing) return;
isDrawing = false;
const x = Math.min(startX, endX);
const y = Math.min(startY, endY);
const w = Math.abs(endX - startX);
const h = Math.abs(endY - startY);
if (w < 10 || h < 10) {
console.warn('Selection too small');
return;
}
const croppedCanvas = document.createElement('canvas');
croppedCanvas.width = w * scaleX;
croppedCanvas.height = h * scaleY;
const croppedCtx = croppedCanvas.getContext('2d');
croppedCtx.drawImage(fullCanvas, x * scaleX, y * scaleY, w * scaleX, h * scaleY, 0, 0, w * scaleX, h * scaleY);
cleanup();
resolve(croppedCanvas);
});
fullScreenBtn.addEventListener('click', () => {
cleanup();
resolve(fullCanvas);
});
snippetBtn.addEventListener('click', () => {
snippingMode = true;
overlayCanvas.style.display = 'block';
controlPanel.style.display = 'none';
console.log('Snipping mode active: Draw rectangle to select. ESC to cancel.');
});
cancelBtn.addEventListener('click', () => {
cleanup();
resolve(null);
});
const handleKeydown = (e) => {
if (e.key === 'Escape') {
if (snippingMode) {
snippingMode = false;
overlayCanvas.style.display = 'none';
controlPanel.style.display = 'flex';
ctx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
startX = startY = endX = endY = 0;
} else {
cleanup();
resolve(null);
}
}
};
function cleanup() {
document.removeEventListener('keydown', handleKeydown);
container.remove();
}
document.addEventListener('keydown', handleKeydown);
};
};
video.play();
}).catch(error => {
console.error('Capture failed:', error);
resolve(null);
});
});
};
window.canvasToFile = async function(canvas, filename = null) {
console.log('canvasToFile called');
return new Promise((resolve) => {
const name = filename || `screenshot-${Date.now()}.png`;
canvas.toBlob((blob) => {
const file = new File([blob], name, { type: 'image/png' });
resolve(file);
});
});
};
console.log('✓ Capture tools initialized successfully');
console.log('Available: window.captureScreenshot, window.captureSnip, window.canvasToFile');
</script>
<script src="./tools/screen-record/index.js"></script>
</head>
<body class="bg-gradient-to-br from-primary to-secondary p-4 sm:p-8 flex flex-col items-center justify-center font-sans relative h-screen overflow-hidden">
<!-- Top-right display name -->
@@ -172,7 +401,7 @@
</div>
</div>
</div>
<div class="space-y-2 relative pb-32">
<div class="space-y-2 relative pb-32 -mt-8 gap-2">
<div class="flex items-center gap-3">
<button id="stopBtn" class="hidden w-10 h-10 flex items-center justify-center rounded-full bg-red-600 text-white shadow disabled:opacity-50" title="Stop recording" disabled>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -210,15 +439,13 @@
</div>
<div id="recordStatus" class="text-xs text-gray-500"></div>
</div>
<div class="space-y-2 -mt-8">
<label class="block text-sm font-medium text-gray-700">Attachments</label>
<div class="space-y-2">
<div class="relative">
<div class="flex items-center gap-2">
<button id="attachmentMenuBtn" class="px-3 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-100 transition flex items-center gap-2" type="button" title="Add attachment">
<button id="attachmentMenuBtn" class="px-2 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-100 transition flex items-center gap-2" type="button" title="Add attachment">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-5 h-5">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
</svg>
Attach
</button>
<button id="clearAttachmentsBtn" class="px-3 py-1 rounded border border-gray-200 text-gray-700 hover:bg-gray-100 text-sm" type="button">Clear</button>
<div id="noteAttachmentsSummary" class="text-xs text-gray-600">No attachments selected.</div>
@@ -238,12 +465,18 @@
</svg>
</button>
<div id="imageSubmenu" class="hidden bg-gray-50 border-t border-gray-200">
<button id="takePhotoBtn" class="w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<button id="takePhotoBtn" class="mobile-only w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>
</svg>
Take Photo
</button>
<button id="takeSnipBtn" class="desktop-only hidden w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<path d="M3 3h6v6H3zM15 3h6v6h-6zM3 15h6v6H3zM15 15h6v6h-6z"/><line x1="9" y1="3" x2="9" y2="24"/><line x1="15" y1="3" x2="15" y2="24"/><line x1="3" y1="9" x2="24" y2="9"/><line x1="3" y1="15" x2="24" y2="15"/>
</svg>
Screenshot/Clip
</button>
<button id="selectImageBtn" class="w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/>
@@ -263,12 +496,18 @@
</svg>
</button>
<div id="videoSubmenu" class="hidden bg-gray-50 border-t border-gray-200">
<button id="recordVideoBtn" class="w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<button id="recordVideoBtn" class="mobile-only w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<circle cx="12" cy="13" r="9"/><path d="M12 10v6"/>
</svg>
Take Video
</button>
<button id="recordScreenBtn" class="desktop-only hidden w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><circle cx="12" cy="10" r="2"/>
</svg>
Record Screen
</button>
<button id="selectVideoBtn" class="w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/>
@@ -416,14 +655,12 @@
</div>
<div class="space-y-2">
<label class="block text-sm font-medium text-gray-700">Attachments</label>
<div class="relative">
<div class="flex items-center gap-2">
<button id="conversationAttachmentMenuBtn" class="px-3 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-100 transition flex items-center gap-2" type="button" title="Add attachment">
<button id="conversationAttachmentMenuBtn" class="px-2 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-100 transition flex items-center gap-2" type="button" title="Add attachment">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-5 h-5">
<path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2"/>
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/>
</svg>
Attach
</button>
<button id="conversationClearAttachmentsBtn" class="px-3 py-1 rounded border border-gray-200 text-gray-700 hover:bg-gray-100 text-sm" type="button">Clear</button>
<div id="conversationAttachmentsSummary" class="text-xs text-gray-600">No attachments selected.</div>
@@ -443,12 +680,18 @@
</svg>
</button>
<div id="conversationImageSubmenu" class="hidden bg-gray-50 border-t border-gray-200">
<button id="conversationTakePhotoBtn" class="w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<button id="conversationTakePhotoBtn" class="mobile-only w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>
</svg>
Take Photo
</button>
<button id="conversationTakeSnipBtn" class="desktop-only hidden w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<path d="M3 3h6v6H3zM15 3h6v6h-6zM3 15h6v6H3zM15 15h6v6h-6z"/><line x1="9" y1="3" x2="9" y2="24"/><line x1="15" y1="3" x2="15" y2="24"/><line x1="3" y1="9" x2="24" y2="9"/><line x1="3" y1="15" x2="24" y2="15"/>
</svg>
Screenshot/Clip
</button>
<button id="conversationSelectImageBtn" class="w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/>
@@ -468,12 +711,18 @@
</svg>
</button>
<div id="conversationVideoSubmenu" class="hidden bg-gray-50 border-t border-gray-200">
<button id="conversationRecordVideoBtn" class="w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<button id="conversationRecordVideoBtn" class="mobile-only w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<circle cx="12" cy="13" r="9"/><path d="M12 10v6"/>
</svg>
Take Video
</button>
<button id="conversationRecordScreenBtn" class="desktop-only hidden w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><circle cx="12" cy="10" r="2"/>
</svg>
Record Screen
</button>
<button id="conversationSelectVideoBtn" class="w-full text-left px-6 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center gap-2" type="button">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="w-4 h-4">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/>
@@ -1794,6 +2043,19 @@
const videoCameraInput = document.getElementById('videoCameraInput');
const fileInput = document.getElementById('fileInput');
// Detect mobile vs desktop and show/hide appropriate buttons
const isMobileDevice = () => /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const setupDeviceSpecificUI = () => {
if (isMobileDevice()) {
document.querySelectorAll('.desktop-only').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.mobile-only').forEach(el => el.classList.remove('hidden'));
} else {
document.querySelectorAll('.mobile-only').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.desktop-only').forEach(el => el.classList.remove('hidden'));
}
};
setupDeviceSpecificUI();
attachmentMenuBtn.addEventListener('click', (e) => {
e.preventDefault();
attachmentMenu.classList.toggle('hidden');
@@ -1862,6 +2124,42 @@
renderNoteAttachmentsList();
});
// Snip handler (partial screenshot)
document.getElementById('takeSnipBtn')?.addEventListener('click', async () => {
try {
attachmentMenu.classList.add('hidden');
imageSubmenu.classList.add('hidden');
const canvas = await window.captureSnip();
if (canvas) {
const file = await window.canvasToFile(canvas);
pendingNoteAttachments.push(file);
renderNoteAttachmentsList();
}
} catch (error) {
console.error('Snip failed:', error);
}
});
// Screen recording handler
document.getElementById('recordScreenBtn')?.addEventListener('click', async () => {
try {
attachmentMenu.classList.add('hidden');
videoSubmenu.classList.add('hidden');
const recorder = await window.recordScreen();
if (recorder) {
// Auto-stop after 5 minutes
setTimeout(async () => {
const file = await recorder.stop();
pendingNoteAttachments.push(file);
renderNoteAttachmentsList();
}, 5 * 60 * 1000);
}
} catch (error) {
console.error('Screen recording failed:', error);
}
});
// Close menu on outside click
document.addEventListener('click', (e) => {
if (!attachmentMenuBtn.contains(e.target) && !attachmentMenu.contains(e.target)) {
@@ -1948,6 +2246,42 @@
renderConversationAttachmentsList();
});
// Snip handler for conversation (partial screenshot)
document.getElementById('conversationTakeSnipBtn')?.addEventListener('click', async () => {
try {
conversationAttachmentMenu.classList.add('hidden');
conversationImageSubmenu.classList.add('hidden');
const canvas = await window.captureSnip();
if (canvas) {
const file = await window.canvasToFile(canvas);
pendingConversationAttachments.push(file);
renderConversationAttachmentsList();
}
} catch (error) {
console.error('Snip failed:', error);
}
});
// Screen recording handler for conversation
document.getElementById('conversationRecordScreenBtn')?.addEventListener('click', async () => {
try {
conversationAttachmentMenu.classList.add('hidden');
conversationVideoSubmenu.classList.add('hidden');
const recorder = await window.recordScreen();
if (recorder) {
// Auto-stop after 5 minutes
setTimeout(async () => {
const file = await recorder.stop();
pendingConversationAttachments.push(file);
renderConversationAttachmentsList();
}, 5 * 60 * 1000);
}
} catch (error) {
console.error('Screen recording failed:', error);
}
});
// Close conversation menu on outside click
document.addEventListener('click', (e) => {
if (!conversationAttachmentMenuBtn.contains(e.target) && !conversationAttachmentMenu.contains(e.target)) {