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.
@@ -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
+ }