electrobun 0.0.19-beta.112 → 0.0.19-beta.114
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/package.json +1 -1
- package/src/cli/index.ts +1 -1
- package/templates/hello-world/bun.lock +164 -2
- package/templates/hello-world/electrobun.config +4 -4
- package/templates/hello-world/package.json +1 -1
- package/templates/hello-world/src/bun/index.ts +2 -2
- package/templates/hello-world/src/mainview/index.html +5 -9
- package/templates/hello-world/src/mainview/index.ts +1 -24
- package/templates/photo-booth/bun.lock +16 -2
- package/templates/photo-booth/package.json +1 -1
- package/templates/photo-booth/src/bun/index.ts +2 -1
- package/templates/photo-booth/src/mainview/index.css +112 -0
- package/templates/photo-booth/src/mainview/index.html +38 -9
- package/templates/photo-booth/src/mainview/index.ts +284 -369
- package/templates/photo-booth/src/mainview/index_old.ts +671 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import Electrobun, { Electroview } from "electrobun/view";
|
|
2
|
+
import type { PhotoBoothRPC } from "../bun/index";
|
|
3
|
+
|
|
4
|
+
// Create RPC client
|
|
5
|
+
const rpc = Electroview.defineRPC<PhotoBoothRPC>({
|
|
6
|
+
maxRequestTime: 5000,
|
|
7
|
+
handlers: {
|
|
8
|
+
requests: {},
|
|
9
|
+
messages: {}
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const electrobun = new Electrobun.Electroview({ rpc });
|
|
14
|
+
|
|
15
|
+
interface Photo {
|
|
16
|
+
id: string;
|
|
17
|
+
dataUrl: string;
|
|
18
|
+
timestamp: Date;
|
|
19
|
+
type: 'camera' | 'screen';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type CaptureMode = 'camera' | 'screen';
|
|
23
|
+
|
|
24
|
+
class PhotoBooth {
|
|
25
|
+
private video: HTMLVideoElement;
|
|
26
|
+
private canvas: HTMLCanvasElement;
|
|
27
|
+
private captureBtn: HTMLButtonElement;
|
|
28
|
+
private gallery: HTMLElement;
|
|
29
|
+
private cameraSelect: HTMLSelectElement;
|
|
30
|
+
private timerToggle: HTMLInputElement;
|
|
31
|
+
private cameraModeBtn: HTMLButtonElement;
|
|
32
|
+
private screenModeBtn: HTMLButtonElement;
|
|
33
|
+
private startCameraBtn: HTMLButtonElement;
|
|
34
|
+
private selectScreenBtn: HTMLButtonElement;
|
|
35
|
+
private status: HTMLElement;
|
|
36
|
+
private statusText: HTMLElement;
|
|
37
|
+
private countdown: HTMLElement;
|
|
38
|
+
private modal: HTMLElement;
|
|
39
|
+
private modalImage: HTMLImageElement;
|
|
40
|
+
private captureBtnText: HTMLElement;
|
|
41
|
+
private cameraIcon: HTMLElement;
|
|
42
|
+
private screenIcon: HTMLElement;
|
|
43
|
+
|
|
44
|
+
private stream: MediaStream | null = null;
|
|
45
|
+
private photos: Photo[] = [];
|
|
46
|
+
private currentPhotoId: string | null = null;
|
|
47
|
+
private currentMode: CaptureMode = 'camera';
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
// Get DOM elements
|
|
51
|
+
this.video = document.getElementById('video') as HTMLVideoElement;
|
|
52
|
+
this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
|
53
|
+
this.captureBtn = document.getElementById('captureBtn') as HTMLButtonElement;
|
|
54
|
+
this.gallery = document.getElementById('gallery') as HTMLElement;
|
|
55
|
+
this.cameraSelect = document.getElementById('cameraSelect') as HTMLSelectElement;
|
|
56
|
+
this.timerToggle = document.getElementById('timerToggle') as HTMLInputElement;
|
|
57
|
+
this.cameraModeBtn = document.getElementById('cameraModeBtn') as HTMLButtonElement;
|
|
58
|
+
this.screenModeBtn = document.getElementById('screenModeBtn') as HTMLButtonElement;
|
|
59
|
+
this.startCameraBtn = document.getElementById('startCameraBtn') as HTMLButtonElement;
|
|
60
|
+
this.selectScreenBtn = document.getElementById('selectScreenBtn') as HTMLButtonElement;
|
|
61
|
+
this.status = document.getElementById('status') as HTMLElement;
|
|
62
|
+
this.statusText = this.status.querySelector('.status-text') as HTMLElement;
|
|
63
|
+
this.countdown = document.getElementById('countdown') as HTMLElement;
|
|
64
|
+
this.modal = document.getElementById('photoModal') as HTMLElement;
|
|
65
|
+
this.modalImage = document.getElementById('modalImage') as HTMLImageElement;
|
|
66
|
+
this.captureBtnText = this.captureBtn.querySelector('.capture-btn-text') as HTMLElement;
|
|
67
|
+
this.cameraIcon = this.captureBtn.querySelector('.capture-icon-camera') as HTMLElement;
|
|
68
|
+
this.screenIcon = this.captureBtn.querySelector('.capture-icon-screen') as HTMLElement;
|
|
69
|
+
|
|
70
|
+
this.initializeEventListeners();
|
|
71
|
+
this.initializeApp();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private initializeEventListeners() {
|
|
75
|
+
// Mode toggle buttons
|
|
76
|
+
this.cameraModeBtn.addEventListener('click', () => this.setMode('camera'));
|
|
77
|
+
this.screenModeBtn.addEventListener('click', () => this.setMode('screen'));
|
|
78
|
+
|
|
79
|
+
// Capture button
|
|
80
|
+
this.captureBtn.addEventListener('click', () => this.capturePhoto());
|
|
81
|
+
|
|
82
|
+
// Camera controls
|
|
83
|
+
this.startCameraBtn.addEventListener('click', () => this.startCamera());
|
|
84
|
+
this.cameraSelect.addEventListener('change', (e) => {
|
|
85
|
+
const deviceId = (e.target as HTMLSelectElement).value;
|
|
86
|
+
if (deviceId) {
|
|
87
|
+
this.switchCamera(deviceId);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Screen controls
|
|
92
|
+
this.selectScreenBtn.addEventListener('click', () => this.selectScreen());
|
|
93
|
+
|
|
94
|
+
// Modal controls
|
|
95
|
+
document.getElementById('modalClose')?.addEventListener('click', () => this.closeModal());
|
|
96
|
+
document.getElementById('downloadBtn')?.addEventListener('click', () => this.saveCurrentPhoto());
|
|
97
|
+
document.getElementById('deleteBtn')?.addEventListener('click', () => this.deleteCurrentPhoto());
|
|
98
|
+
|
|
99
|
+
// Close modal on background click
|
|
100
|
+
this.modal.addEventListener('click', (e) => {
|
|
101
|
+
if (e.target === this.modal) {
|
|
102
|
+
this.closeModal();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async initializeApp() {
|
|
108
|
+
// Set initial mode
|
|
109
|
+
this.setMode('camera');
|
|
110
|
+
|
|
111
|
+
// Check available cameras
|
|
112
|
+
await this.populateCameraList();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private setMode(mode: CaptureMode) {
|
|
116
|
+
this.currentMode = mode;
|
|
117
|
+
|
|
118
|
+
// Update UI classes
|
|
119
|
+
document.body.classList.toggle('mode-screen', mode === 'screen');
|
|
120
|
+
|
|
121
|
+
// Update mode buttons
|
|
122
|
+
this.cameraModeBtn.classList.toggle('active', mode === 'camera');
|
|
123
|
+
this.screenModeBtn.classList.toggle('active', mode === 'screen');
|
|
124
|
+
|
|
125
|
+
// Update capture button
|
|
126
|
+
this.cameraIcon.style.display = mode === 'camera' ? 'block' : 'none';
|
|
127
|
+
this.screenIcon.style.display = mode === 'screen' ? 'block' : 'none';
|
|
128
|
+
this.captureBtnText.textContent = mode === 'camera' ? 'Take Photo' : 'Take Screenshot';
|
|
129
|
+
|
|
130
|
+
// Reset state when switching modes
|
|
131
|
+
this.stopStream();
|
|
132
|
+
this.captureBtn.disabled = true;
|
|
133
|
+
|
|
134
|
+
// Update status based on mode
|
|
135
|
+
if (mode === 'camera') {
|
|
136
|
+
this.setStatus('Click "Start Camera" to begin', false);
|
|
137
|
+
this.startCameraBtn.style.display = 'flex';
|
|
138
|
+
} else {
|
|
139
|
+
this.setStatus('Click "Select Screen" to choose what to capture', false);
|
|
140
|
+
this.selectScreenBtn.style.display = 'flex';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async populateCameraList() {
|
|
145
|
+
try {
|
|
146
|
+
// First, get available cameras without requesting permission
|
|
147
|
+
await this.getAvailableCameras();
|
|
148
|
+
|
|
149
|
+
// Check if cameras are available but don't request permission yet
|
|
150
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
151
|
+
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
|
152
|
+
|
|
153
|
+
if (videoDevices.length > 0) {
|
|
154
|
+
// Camera available but don't access it until user clicks capture
|
|
155
|
+
this.setStatus('Camera available - click capture to start', false);
|
|
156
|
+
this.captureBtn.disabled = false;
|
|
157
|
+
} else {
|
|
158
|
+
console.log('No cameras found, enabling screen capture mode');
|
|
159
|
+
// No cameras found, enable screen capture mode
|
|
160
|
+
this.enableScreenCaptureMode();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.log('Camera enumeration failed, enabling screen capture mode:', error);
|
|
165
|
+
// If camera enumeration fails, enable screen capture mode
|
|
166
|
+
this.enableScreenCaptureMode();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private enableScreenCaptureMode() {
|
|
171
|
+
// Check what media APIs are available
|
|
172
|
+
console.log('MediaDevices available:', !!navigator.mediaDevices);
|
|
173
|
+
console.log('getUserMedia available:', !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia));
|
|
174
|
+
console.log('getDisplayMedia available:', !!(navigator.mediaDevices && (navigator.mediaDevices as any).getDisplayMedia));
|
|
175
|
+
console.log('WebRTC APIs available:', !!window.RTCPeerConnection);
|
|
176
|
+
|
|
177
|
+
this.setStatus('Screen capture mode', true);
|
|
178
|
+
this.captureBtn.disabled = false;
|
|
179
|
+
|
|
180
|
+
// Hide camera selector and show change source button
|
|
181
|
+
this.cameraSelect.style.display = 'none';
|
|
182
|
+
this.changeSourceBtn.style.display = 'flex';
|
|
183
|
+
|
|
184
|
+
// Update UI to indicate screen capture mode
|
|
185
|
+
const captureBtn = this.captureBtn;
|
|
186
|
+
captureBtn.innerHTML = `
|
|
187
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
188
|
+
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
|
189
|
+
<line x1="8" y1="21" x2="16" y2="21"/>
|
|
190
|
+
<line x1="12" y1="17" x2="12" y2="21"/>
|
|
191
|
+
</svg>
|
|
192
|
+
Take Screenshot
|
|
193
|
+
`;
|
|
194
|
+
|
|
195
|
+
if (navigator.mediaDevices && (navigator.mediaDevices as any).getDisplayMedia) {
|
|
196
|
+
this.showStatus('No camera found. Click "Take Screenshot" to capture your screen instead!', 'info');
|
|
197
|
+
|
|
198
|
+
console.log('getDisplayMedia is available - ready for screen capture');
|
|
199
|
+
} else {
|
|
200
|
+
// getDisplayMedia not available, but we can use native screen capture
|
|
201
|
+
this.showStatus('Using native screen capture. Click "Take Screenshot" to capture your screen!', 'info');
|
|
202
|
+
this.captureBtn.disabled = false; // Enable the button for native capture
|
|
203
|
+
console.log('getDisplayMedia not available, will use native screen capture fallback');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async tryCamera() {
|
|
208
|
+
// Request camera permission and start stream
|
|
209
|
+
const constraints: MediaStreamConstraints = {
|
|
210
|
+
video: {
|
|
211
|
+
width: { ideal: 1280 },
|
|
212
|
+
height: { ideal: 720 }
|
|
213
|
+
// Removed facingMode constraint as it's not supported on most desktop cameras
|
|
214
|
+
},
|
|
215
|
+
audio: false
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
219
|
+
this.video.srcObject = this.stream;
|
|
220
|
+
|
|
221
|
+
// Update status
|
|
222
|
+
this.setStatus('Camera active', true);
|
|
223
|
+
this.captureBtn.disabled = false;
|
|
224
|
+
|
|
225
|
+
// Update camera list with active camera
|
|
226
|
+
const videoTrack = this.stream.getVideoTracks()[0];
|
|
227
|
+
const settings = videoTrack.getSettings();
|
|
228
|
+
if (settings.deviceId) {
|
|
229
|
+
this.cameraSelect.value = settings.deviceId;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private async tryScreenCapture() {
|
|
234
|
+
console.log('tryScreenCapture called');
|
|
235
|
+
|
|
236
|
+
// Stop existing stream if any
|
|
237
|
+
if (this.stream) {
|
|
238
|
+
console.log('Stopping existing stream');
|
|
239
|
+
this.stream.getTracks().forEach(track => track.stop());
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if getDisplayMedia is available
|
|
243
|
+
if (!navigator.mediaDevices || !(navigator.mediaDevices as any).getDisplayMedia) {
|
|
244
|
+
throw new Error('getDisplayMedia not supported in this browser');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
console.log('Requesting display media...');
|
|
248
|
+
// Request screen capture (requires user gesture)
|
|
249
|
+
this.stream = await (navigator.mediaDevices as any).getDisplayMedia({
|
|
250
|
+
video: true,
|
|
251
|
+
audio: false
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
console.log('Display media obtained:', this.stream);
|
|
255
|
+
|
|
256
|
+
this.video.srcObject = this.stream;
|
|
257
|
+
|
|
258
|
+
// Wait for video to be ready
|
|
259
|
+
await new Promise<void>((resolve) => {
|
|
260
|
+
const onLoadedData = () => {
|
|
261
|
+
this.video.removeEventListener('loadeddata', onLoadedData);
|
|
262
|
+
resolve();
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (this.video.readyState >= 2) {
|
|
266
|
+
// Video is already loaded
|
|
267
|
+
resolve();
|
|
268
|
+
} else {
|
|
269
|
+
this.video.addEventListener('loadeddata', onLoadedData);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Update status to indicate screen capture
|
|
274
|
+
this.setStatus('Screen capture active', true);
|
|
275
|
+
this.captureBtn.disabled = false;
|
|
276
|
+
|
|
277
|
+
// Hide camera selector and show change source button
|
|
278
|
+
this.cameraSelect.style.display = 'none';
|
|
279
|
+
this.changeSourceBtn.style.display = 'flex';
|
|
280
|
+
|
|
281
|
+
// Listen for when the user stops sharing (e.g., closes the share dialog)
|
|
282
|
+
this.stream.getVideoTracks()[0].addEventListener('ended', () => {
|
|
283
|
+
this.setStatus('Screen sharing stopped', false, true);
|
|
284
|
+
this.showStatus('Screen sharing was stopped. Click "Change Source" to select a new source.', 'info');
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private async changeScreenSource() {
|
|
289
|
+
console.log('Change source button clicked');
|
|
290
|
+
try {
|
|
291
|
+
console.log('Attempting screen capture...');
|
|
292
|
+
// Always try screen capture when user explicitly asks to change source
|
|
293
|
+
await this.tryScreenCapture();
|
|
294
|
+
console.log('Screen capture successful');
|
|
295
|
+
this.showStatus('Screen source changed successfully!', 'success');
|
|
296
|
+
} catch (error) {
|
|
297
|
+
console.error('Failed to change screen source:', error);
|
|
298
|
+
this.showStatus(`Screen capture failed: ${error.message}`, 'error');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async getAvailableCameras() {
|
|
303
|
+
try {
|
|
304
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
305
|
+
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
|
306
|
+
|
|
307
|
+
// Clear existing options
|
|
308
|
+
this.cameraSelect.innerHTML = '<option value="">Select Camera</option>';
|
|
309
|
+
|
|
310
|
+
// Add camera options
|
|
311
|
+
videoDevices.forEach((device, index) => {
|
|
312
|
+
const option = document.createElement('option');
|
|
313
|
+
option.value = device.deviceId;
|
|
314
|
+
option.textContent = device.label || `Camera ${index + 1}`;
|
|
315
|
+
this.cameraSelect.appendChild(option);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Show/hide camera selector based on available cameras
|
|
319
|
+
this.cameraSelect.style.display = videoDevices.length > 1 ? 'block' : 'none';
|
|
320
|
+
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error('Error enumerating devices:', error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private async switchCamera(deviceId: string) {
|
|
327
|
+
try {
|
|
328
|
+
// Stop current stream
|
|
329
|
+
if (this.stream) {
|
|
330
|
+
this.stream.getTracks().forEach(track => track.stop());
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Start new stream with selected camera
|
|
334
|
+
const constraints: MediaStreamConstraints = {
|
|
335
|
+
video: {
|
|
336
|
+
deviceId: { exact: deviceId },
|
|
337
|
+
width: { ideal: 1280 },
|
|
338
|
+
height: { ideal: 720 }
|
|
339
|
+
},
|
|
340
|
+
audio: false
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
this.stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
344
|
+
this.video.srcObject = this.stream;
|
|
345
|
+
|
|
346
|
+
// Wait for video to be ready after switching
|
|
347
|
+
await new Promise<void>((resolve) => {
|
|
348
|
+
const onLoadedData = () => {
|
|
349
|
+
this.video.removeEventListener('loadeddata', onLoadedData);
|
|
350
|
+
resolve();
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
if (this.video.readyState >= 2) {
|
|
354
|
+
// Video is already loaded
|
|
355
|
+
resolve();
|
|
356
|
+
} else {
|
|
357
|
+
this.video.addEventListener('loadeddata', onLoadedData);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
this.setStatus('Camera switched', true);
|
|
362
|
+
this.showStatus('Camera switched successfully!', 'success');
|
|
363
|
+
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('Error switching camera:', error);
|
|
366
|
+
this.showStatus('Failed to switch camera', 'error');
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private async capturePhoto() {
|
|
371
|
+
// Check if we're in screen capture mode and getDisplayMedia is not available
|
|
372
|
+
if (this.stream === null && this.changeSourceBtn.style.display === 'flex') {
|
|
373
|
+
// We're in screen capture mode but no stream - use native capture
|
|
374
|
+
await this.captureNativeScreenshot();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
console.log('capturePhoto called, stream exists:', !!this.stream);
|
|
378
|
+
|
|
379
|
+
// If no stream is available, try to get one (camera or screen capture)
|
|
380
|
+
if (!this.stream) {
|
|
381
|
+
console.log('No stream available, trying to get one...');
|
|
382
|
+
try {
|
|
383
|
+
// Check if cameras are available first
|
|
384
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
385
|
+
const videoDevices = devices.filter(device => device.kind === 'videoinput');
|
|
386
|
+
|
|
387
|
+
if (videoDevices.length > 0) {
|
|
388
|
+
// Try camera first
|
|
389
|
+
console.log('Cameras available, trying camera...');
|
|
390
|
+
await this.tryCamera();
|
|
391
|
+
} else {
|
|
392
|
+
// No cameras, try screen capture
|
|
393
|
+
console.log('No cameras available, trying screen capture...');
|
|
394
|
+
await this.tryScreenCapture();
|
|
395
|
+
}
|
|
396
|
+
console.log('Stream obtained successfully');
|
|
397
|
+
} catch (error) {
|
|
398
|
+
console.error('Failed to get stream in capturePhoto:', error);
|
|
399
|
+
this.showStatus(`Failed to get camera/screen: ${error.message}`, 'error');
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (this.timerToggle.checked) {
|
|
405
|
+
// Use timer
|
|
406
|
+
this.captureBtn.disabled = true;
|
|
407
|
+
await this.runCountdown();
|
|
408
|
+
this.takePhoto();
|
|
409
|
+
this.captureBtn.disabled = false;
|
|
410
|
+
} else {
|
|
411
|
+
// Immediate capture
|
|
412
|
+
this.takePhoto();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
private async runCountdown() {
|
|
417
|
+
for (let i = 3; i > 0; i--) {
|
|
418
|
+
this.countdown.textContent = i.toString();
|
|
419
|
+
this.countdown.classList.add('active');
|
|
420
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
421
|
+
}
|
|
422
|
+
this.countdown.classList.remove('active');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
private takePhoto() {
|
|
426
|
+
console.log('Taking photo - video dimensions:', this.video.videoWidth, 'x', this.video.videoHeight);
|
|
427
|
+
console.log('Video readyState:', this.video.readyState);
|
|
428
|
+
console.log('Stream state:', this.stream ? 'exists' : 'null');
|
|
429
|
+
|
|
430
|
+
// Check if video is ready - give it more time if needed
|
|
431
|
+
if (!this.video.videoWidth || !this.video.videoHeight) {
|
|
432
|
+
// Try waiting a bit more for the video to load
|
|
433
|
+
setTimeout(() => {
|
|
434
|
+
if (this.video.videoWidth && this.video.videoHeight) {
|
|
435
|
+
console.log('Video ready after timeout, retrying...');
|
|
436
|
+
this.takePhoto();
|
|
437
|
+
} else {
|
|
438
|
+
this.showStatus(`Video not ready (${this.video.videoWidth}x${this.video.videoHeight}). Please try "Change Source".`, 'error');
|
|
439
|
+
}
|
|
440
|
+
}, 1000);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Check if stream is still active
|
|
445
|
+
if (!this.stream || this.stream.getVideoTracks().length === 0) {
|
|
446
|
+
this.showStatus('Stream not active. Please change source and try again.', 'error');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const videoTrack = this.stream.getVideoTracks()[0];
|
|
451
|
+
if (!videoTrack || videoTrack.readyState !== 'live') {
|
|
452
|
+
this.showStatus('Video track not live. Please change source and try again.', 'error');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Set canvas size to match video
|
|
457
|
+
this.canvas.width = this.video.videoWidth;
|
|
458
|
+
this.canvas.height = this.video.videoHeight;
|
|
459
|
+
|
|
460
|
+
// Draw video frame to canvas
|
|
461
|
+
const ctx = this.canvas.getContext('2d');
|
|
462
|
+
if (!ctx) {
|
|
463
|
+
this.showStatus('Canvas not available', 'error');
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
ctx.drawImage(this.video, 0, 0);
|
|
468
|
+
|
|
469
|
+
// Convert to data URL
|
|
470
|
+
const dataUrl = this.canvas.toDataURL('image/png');
|
|
471
|
+
|
|
472
|
+
// Verify we got valid image data
|
|
473
|
+
if (dataUrl === 'data:,' || dataUrl.length < 100) {
|
|
474
|
+
this.showStatus('Failed to capture image data. Please try again.', 'error');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Create photo object
|
|
479
|
+
const photo: Photo = {
|
|
480
|
+
id: Date.now().toString(),
|
|
481
|
+
dataUrl: dataUrl,
|
|
482
|
+
timestamp: new Date()
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// Add to photos array
|
|
486
|
+
this.photos.unshift(photo);
|
|
487
|
+
|
|
488
|
+
// Update gallery
|
|
489
|
+
this.updateGallery();
|
|
490
|
+
|
|
491
|
+
// Flash effect
|
|
492
|
+
this.flashEffect();
|
|
493
|
+
|
|
494
|
+
// Show success message
|
|
495
|
+
this.showStatus('Screenshot captured!', 'success');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private flashEffect() {
|
|
499
|
+
const flash = document.createElement('div');
|
|
500
|
+
flash.style.cssText = `
|
|
501
|
+
position: fixed;
|
|
502
|
+
top: 0;
|
|
503
|
+
left: 0;
|
|
504
|
+
right: 0;
|
|
505
|
+
bottom: 0;
|
|
506
|
+
background: white;
|
|
507
|
+
opacity: 0.8;
|
|
508
|
+
z-index: 999;
|
|
509
|
+
pointer-events: none;
|
|
510
|
+
`;
|
|
511
|
+
document.body.appendChild(flash);
|
|
512
|
+
|
|
513
|
+
setTimeout(() => {
|
|
514
|
+
flash.style.transition = 'opacity 0.3s';
|
|
515
|
+
flash.style.opacity = '0';
|
|
516
|
+
setTimeout(() => flash.remove(), 300);
|
|
517
|
+
}, 100);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private updateGallery() {
|
|
521
|
+
if (this.photos.length === 0) {
|
|
522
|
+
this.gallery.innerHTML = '<div class="empty-state">No photos/screenshots yet. Click the capture button to get started!</div>';
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
this.gallery.innerHTML = this.photos.map(photo => `
|
|
527
|
+
<div class="photo-item" data-id="${photo.id}">
|
|
528
|
+
<img src="${photo.dataUrl}" alt="Photo ${photo.id}">
|
|
529
|
+
<div class="photo-time">${this.formatTime(photo.timestamp)}</div>
|
|
530
|
+
</div>
|
|
531
|
+
`).join('');
|
|
532
|
+
|
|
533
|
+
// Add click listeners to photos
|
|
534
|
+
this.gallery.querySelectorAll('.photo-item').forEach(item => {
|
|
535
|
+
item.addEventListener('click', () => {
|
|
536
|
+
const id = item.getAttribute('data-id');
|
|
537
|
+
if (id) this.openModal(id);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private formatTime(date: Date): string {
|
|
543
|
+
return date.toLocaleTimeString('en-US', {
|
|
544
|
+
hour: '2-digit',
|
|
545
|
+
minute: '2-digit'
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private openModal(photoId: string) {
|
|
550
|
+
const photo = this.photos.find(p => p.id === photoId);
|
|
551
|
+
if (!photo) return;
|
|
552
|
+
|
|
553
|
+
this.currentPhotoId = photoId;
|
|
554
|
+
this.modalImage.src = photo.dataUrl;
|
|
555
|
+
this.modal.classList.add('active');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private closeModal() {
|
|
559
|
+
this.modal.classList.remove('active');
|
|
560
|
+
this.currentPhotoId = null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private async saveCurrentPhoto() {
|
|
564
|
+
if (!this.currentPhotoId) return;
|
|
565
|
+
|
|
566
|
+
const photo = this.photos.find(p => p.id === this.currentPhotoId);
|
|
567
|
+
if (!photo) return;
|
|
568
|
+
|
|
569
|
+
// Generate filename with timestamp
|
|
570
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
571
|
+
const filename = `photo-booth-${timestamp}.png`;
|
|
572
|
+
|
|
573
|
+
try {
|
|
574
|
+
// Call RPC method to save photo
|
|
575
|
+
if (electrobun.rpc) {
|
|
576
|
+
const result = await electrobun.rpc.request.savePhoto({
|
|
577
|
+
dataUrl: photo.dataUrl,
|
|
578
|
+
filename: filename
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
if (result.success) {
|
|
582
|
+
this.showStatus(`Photo saved to ${result.path}`, 'success');
|
|
583
|
+
} else if (result.reason === 'canceled') {
|
|
584
|
+
this.showStatus('Save canceled', 'info');
|
|
585
|
+
} else {
|
|
586
|
+
this.showStatus(`Failed to save photo: ${result.error}`, 'error');
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
this.showStatus('RPC not available', 'error');
|
|
590
|
+
}
|
|
591
|
+
} catch (error) {
|
|
592
|
+
console.error('Error calling savePhoto RPC:', error);
|
|
593
|
+
this.showStatus('Failed to save photo', 'error');
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private deleteCurrentPhoto() {
|
|
598
|
+
if (!this.currentPhotoId) return;
|
|
599
|
+
|
|
600
|
+
// Remove from photos array
|
|
601
|
+
this.photos = this.photos.filter(p => p.id !== this.currentPhotoId);
|
|
602
|
+
|
|
603
|
+
// Update gallery
|
|
604
|
+
this.updateGallery();
|
|
605
|
+
|
|
606
|
+
// Close modal
|
|
607
|
+
this.closeModal();
|
|
608
|
+
|
|
609
|
+
this.showStatus('Photo deleted', 'info');
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private setStatus(text: string, active: boolean, error: boolean = false) {
|
|
613
|
+
this.statusText.textContent = text;
|
|
614
|
+
this.status.classList.toggle('active', active && !error);
|
|
615
|
+
this.status.classList.toggle('error', error);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private async captureNativeScreenshot() {
|
|
619
|
+
console.log('Using native screen capture fallback');
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
// Call the native screen capture through RPC
|
|
623
|
+
const result = await rpc.bun.captureScreen.query({});
|
|
624
|
+
|
|
625
|
+
if (result.success && result.dataUrl) {
|
|
626
|
+
// Add the screenshot to the gallery
|
|
627
|
+
const photo: Photo = {
|
|
628
|
+
id: Date.now().toString(),
|
|
629
|
+
dataUrl: result.dataUrl,
|
|
630
|
+
timestamp: new Date()
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
this.photos.push(photo);
|
|
634
|
+
this.addPhotoToGallery(photo);
|
|
635
|
+
this.showStatus('Screenshot captured!', 'success');
|
|
636
|
+
|
|
637
|
+
// Play capture sound/animation
|
|
638
|
+
this.playCaptureFeedback();
|
|
639
|
+
} else {
|
|
640
|
+
throw new Error(result.error || 'Failed to capture screen');
|
|
641
|
+
}
|
|
642
|
+
} catch (error) {
|
|
643
|
+
console.error('Native screen capture failed:', error);
|
|
644
|
+
this.showStatus(`Screen capture failed: ${error.message}`, 'error');
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private showStatus(message: string, type: 'success' | 'error' | 'info') {
|
|
649
|
+
// You could implement a toast notification here
|
|
650
|
+
console.log(`[${type}] ${message}`);
|
|
651
|
+
|
|
652
|
+
// Update status bar temporarily
|
|
653
|
+
const originalText = this.statusText.textContent;
|
|
654
|
+
const originalClasses = this.status.className;
|
|
655
|
+
|
|
656
|
+
this.statusText.textContent = message;
|
|
657
|
+
this.status.className = 'status ' + type;
|
|
658
|
+
|
|
659
|
+
setTimeout(() => {
|
|
660
|
+
this.statusText.textContent = originalText;
|
|
661
|
+
this.status.className = originalClasses;
|
|
662
|
+
}, 3000);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Initialize app when DOM is ready
|
|
667
|
+
if (document.readyState === 'loading') {
|
|
668
|
+
document.addEventListener('DOMContentLoaded', () => new PhotoBooth());
|
|
669
|
+
} else {
|
|
670
|
+
new PhotoBooth();
|
|
671
|
+
}
|