electrobun 0.0.19-beta.11 → 0.0.19-beta.111

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.
Files changed (46) hide show
  1. package/BUILD.md +90 -0
  2. package/bin/electrobun.cjs +39 -14
  3. package/debug.js +5 -0
  4. package/dist/api/browser/builtinrpcSchema.ts +19 -0
  5. package/dist/api/browser/index.ts +409 -0
  6. package/dist/api/browser/rpc/webview.ts +79 -0
  7. package/dist/api/browser/stylesAndElements.ts +3 -0
  8. package/dist/api/browser/webviewtag.ts +534 -0
  9. package/dist/api/bun/core/ApplicationMenu.ts +66 -0
  10. package/dist/api/bun/core/BrowserView.ts +349 -0
  11. package/dist/api/bun/core/BrowserWindow.ts +191 -0
  12. package/dist/api/bun/core/ContextMenu.ts +67 -0
  13. package/dist/api/bun/core/Paths.ts +5 -0
  14. package/dist/api/bun/core/Socket.ts +181 -0
  15. package/dist/api/bun/core/Tray.ts +107 -0
  16. package/dist/api/bun/core/Updater.ts +680 -0
  17. package/dist/api/bun/core/Utils.ts +48 -0
  18. package/dist/api/bun/events/ApplicationEvents.ts +14 -0
  19. package/dist/api/bun/events/event.ts +29 -0
  20. package/dist/api/bun/events/eventEmitter.ts +45 -0
  21. package/dist/api/bun/events/trayEvents.ts +9 -0
  22. package/dist/api/bun/events/webviewEvents.ts +16 -0
  23. package/dist/api/bun/events/windowEvents.ts +12 -0
  24. package/dist/api/bun/index.ts +45 -0
  25. package/dist/api/bun/proc/linux.md +43 -0
  26. package/dist/api/bun/proc/native.ts +1220 -0
  27. package/dist/api/shared/platform.ts +48 -0
  28. package/dist/main.js +54 -0
  29. package/package.json +9 -6
  30. package/src/cli/index.ts +1227 -223
  31. package/templates/hello-world/README.md +57 -0
  32. package/templates/hello-world/bun.lock +63 -0
  33. package/templates/hello-world/electrobun.config +18 -0
  34. package/templates/hello-world/package.json +16 -0
  35. package/templates/hello-world/src/bun/index.ts +15 -0
  36. package/templates/hello-world/src/mainview/index.css +124 -0
  37. package/templates/hello-world/src/mainview/index.html +50 -0
  38. package/templates/hello-world/src/mainview/index.ts +24 -0
  39. package/templates/photo-booth/README.md +108 -0
  40. package/templates/photo-booth/bun.lock +225 -0
  41. package/templates/photo-booth/electrobun.config +28 -0
  42. package/templates/photo-booth/package.json +16 -0
  43. package/templates/photo-booth/src/bun/index.ts +91 -0
  44. package/templates/photo-booth/src/mainview/index.css +353 -0
  45. package/templates/photo-booth/src/mainview/index.html +95 -0
  46. package/templates/photo-booth/src/mainview/index.ts +584 -0
@@ -0,0 +1,584 @@
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
+ }
20
+
21
+ class PhotoBooth {
22
+ private video: HTMLVideoElement;
23
+ private canvas: HTMLCanvasElement;
24
+ private captureBtn: HTMLButtonElement;
25
+ private gallery: HTMLElement;
26
+ private cameraSelect: HTMLSelectElement;
27
+ private timerToggle: HTMLInputElement;
28
+ private changeSourceBtn: HTMLButtonElement;
29
+ private status: HTMLElement;
30
+ private statusText: HTMLElement;
31
+ private countdown: HTMLElement;
32
+ private modal: HTMLElement;
33
+ private modalImage: HTMLImageElement;
34
+
35
+ private stream: MediaStream | null = null;
36
+ private photos: Photo[] = [];
37
+ private currentPhotoId: string | null = null;
38
+
39
+ constructor() {
40
+ // Get DOM elements
41
+ this.video = document.getElementById('video') as HTMLVideoElement;
42
+ this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
43
+ this.captureBtn = document.getElementById('captureBtn') as HTMLButtonElement;
44
+ this.gallery = document.getElementById('gallery') as HTMLElement;
45
+ this.cameraSelect = document.getElementById('cameraSelect') as HTMLSelectElement;
46
+ this.timerToggle = document.getElementById('timerToggle') as HTMLInputElement;
47
+ this.changeSourceBtn = document.getElementById('changeSourceBtn') as HTMLButtonElement;
48
+ this.status = document.getElementById('status') as HTMLElement;
49
+ this.statusText = this.status.querySelector('.status-text') as HTMLElement;
50
+ this.countdown = document.getElementById('countdown') as HTMLElement;
51
+ this.modal = document.getElementById('photoModal') as HTMLElement;
52
+ this.modalImage = document.getElementById('modalImage') as HTMLImageElement;
53
+
54
+ this.initializeEventListeners();
55
+ this.initializeCamera();
56
+ }
57
+
58
+ private initializeEventListeners() {
59
+ // Capture button
60
+ this.captureBtn.addEventListener('click', (e) => {
61
+ console.log('Capture button clicked - event:', e);
62
+ this.capturePhoto();
63
+ });
64
+
65
+ // Camera selector
66
+ this.cameraSelect.addEventListener('change', (e) => {
67
+ const deviceId = (e.target as HTMLSelectElement).value;
68
+ if (deviceId) {
69
+ this.switchCamera(deviceId);
70
+ }
71
+ });
72
+
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
+ });
79
+
80
+ // Modal controls
81
+ document.getElementById('modalClose')?.addEventListener('click', () => this.closeModal());
82
+ document.getElementById('downloadBtn')?.addEventListener('click', () => this.saveCurrentPhoto());
83
+ document.getElementById('deleteBtn')?.addEventListener('click', () => this.deleteCurrentPhoto());
84
+
85
+ // Close modal on background click
86
+ this.modal.addEventListener('click', (e) => {
87
+ if (e.target === this.modal) {
88
+ this.closeModal();
89
+ }
90
+ });
91
+
92
+ // No need to listen for results - we'll handle them in saveCurrentPhoto
93
+ }
94
+
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
+ }
119
+ }
120
+
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);
127
+
128
+ this.setStatus('Screen capture mode', true);
129
+ this.captureBtn.disabled = false;
130
+
131
+ // Hide camera selector and show change source button
132
+ this.cameraSelect.style.display = 'none';
133
+ this.changeSourceBtn.style.display = 'flex';
134
+
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
+ `;
145
+
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;
153
+ }
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;
179
+ }
180
+ }
181
+
182
+ private async tryScreenCapture() {
183
+ console.log('tryScreenCapture called');
184
+
185
+ // Stop existing stream if any
186
+ if (this.stream) {
187
+ console.log('Stopping existing stream');
188
+ 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');
248
+ }
249
+ }
250
+
251
+ private async getAvailableCameras() {
252
+ try {
253
+ const devices = await navigator.mediaDevices.enumerateDevices();
254
+ const videoDevices = devices.filter(device => device.kind === 'videoinput');
255
+
256
+ // Clear existing options
257
+ this.cameraSelect.innerHTML = '<option value="">Select Camera</option>';
258
+
259
+ // Add camera options
260
+ videoDevices.forEach((device, index) => {
261
+ const option = document.createElement('option');
262
+ option.value = device.deviceId;
263
+ option.textContent = device.label || `Camera ${index + 1}`;
264
+ this.cameraSelect.appendChild(option);
265
+ });
266
+
267
+ // Show/hide camera selector based on available cameras
268
+ this.cameraSelect.style.display = videoDevices.length > 1 ? 'block' : 'none';
269
+
270
+ } catch (error) {
271
+ console.error('Error enumerating devices:', error);
272
+ }
273
+ }
274
+
275
+ private async switchCamera(deviceId: string) {
276
+ 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
+ const constraints: MediaStreamConstraints = {
284
+ video: {
285
+ deviceId: { exact: deviceId },
286
+ width: { ideal: 1280 },
287
+ height: { ideal: 720 }
288
+ },
289
+ audio: false
290
+ };
291
+
292
+ this.stream = await navigator.mediaDevices.getUserMedia(constraints);
293
+ this.video.srcObject = this.stream;
294
+
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');
312
+
313
+ } catch (error) {
314
+ console.error('Error switching camera:', error);
315
+ this.showStatus('Failed to switch camera', 'error');
316
+ }
317
+ }
318
+
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
+ }
345
+ }
346
+
347
+ if (this.timerToggle.checked) {
348
+ // Use timer
349
+ this.captureBtn.disabled = true;
350
+ await this.runCountdown();
351
+ this.takePhoto();
352
+ 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));
364
+ }
365
+ this.countdown.classList.remove('active');
366
+ }
367
+
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');
382
+ }
383
+ }, 1000);
384
+ return;
385
+ }
386
+
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;
397
+ }
398
+
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');
407
+ return;
408
+ }
409
+
410
+ ctx.drawImage(this.video, 0, 0);
411
+
412
+ // Convert to data URL
413
+ const dataUrl = this.canvas.toDataURL('image/png');
414
+
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
+ }
420
+
421
+ // Create photo object
422
+ const photo: Photo = {
423
+ id: Date.now().toString(),
424
+ dataUrl: dataUrl,
425
+ timestamp: new Date()
426
+ };
427
+
428
+ // Add to photos array
429
+ this.photos.unshift(photo);
430
+
431
+ // Update gallery
432
+ this.updateGallery();
433
+
434
+ // Flash effect
435
+ this.flashEffect();
436
+
437
+ // Show success message
438
+ this.showStatus('Screenshot captured!', 'success');
439
+ }
440
+
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);
455
+
456
+ setTimeout(() => {
457
+ flash.style.transition = 'opacity 0.3s';
458
+ flash.style.opacity = '0';
459
+ setTimeout(() => flash.remove(), 300);
460
+ }, 100);
461
+ }
462
+
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;
467
+ }
468
+
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>
473
+ </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
+ }
484
+
485
+ private formatTime(date: Date): string {
486
+ return date.toLocaleTimeString('en-US', {
487
+ hour: '2-digit',
488
+ minute: '2-digit'
489
+ });
490
+ }
491
+
492
+ private openModal(photoId: string) {
493
+ const photo = this.photos.find(p => p.id === photoId);
494
+ if (!photo) return;
495
+
496
+ this.currentPhotoId = photoId;
497
+ this.modalImage.src = photo.dataUrl;
498
+ this.modal.classList.add('active');
499
+ }
500
+
501
+ private closeModal() {
502
+ this.modal.classList.remove('active');
503
+ this.currentPhotoId = null;
504
+ }
505
+
506
+ private async saveCurrentPhoto() {
507
+ if (!this.currentPhotoId) return;
508
+
509
+ const photo = this.photos.find(p => p.id === this.currentPhotoId);
510
+ if (!photo) return;
511
+
512
+ // Generate filename with timestamp
513
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
514
+ const filename = `photo-booth-${timestamp}.png`;
515
+
516
+ 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');
530
+ }
531
+ } else {
532
+ this.showStatus('RPC not available', 'error');
533
+ }
534
+ } catch (error) {
535
+ console.error('Error calling savePhoto RPC:', error);
536
+ this.showStatus('Failed to save photo', 'error');
537
+ }
538
+ }
539
+
540
+ private deleteCurrentPhoto() {
541
+ if (!this.currentPhotoId) return;
542
+
543
+ // Remove from photos array
544
+ this.photos = this.photos.filter(p => p.id !== this.currentPhotoId);
545
+
546
+ // Update gallery
547
+ this.updateGallery();
548
+
549
+ // Close modal
550
+ this.closeModal();
551
+
552
+ this.showStatus('Photo deleted', 'info');
553
+ }
554
+
555
+ private setStatus(text: string, active: boolean, error: boolean = false) {
556
+ this.statusText.textContent = text;
557
+ this.status.classList.toggle('active', active && !error);
558
+ this.status.classList.toggle('error', error);
559
+ }
560
+
561
+ private showStatus(message: string, type: 'success' | 'error' | 'info') {
562
+ // You could implement a toast notification here
563
+ console.log(`[${type}] ${message}`);
564
+
565
+ // Update status bar temporarily
566
+ const originalText = this.statusText.textContent;
567
+ const originalClasses = this.status.className;
568
+
569
+ this.statusText.textContent = message;
570
+ this.status.className = 'status ' + type;
571
+
572
+ setTimeout(() => {
573
+ this.statusText.textContent = originalText;
574
+ this.status.className = originalClasses;
575
+ }, 3000);
576
+ }
577
+ }
578
+
579
+ // Initialize app when DOM is ready
580
+ if (document.readyState === 'loading') {
581
+ document.addEventListener('DOMContentLoaded', () => new PhotoBooth());
582
+ } else {
583
+ new PhotoBooth();
584
+ }