electrobun 0.0.19-beta.113 → 0.0.19-beta.115

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