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.
- package/dist/api/bun/core/Updater.ts +4 -3
- package/package.json +1 -1
- package/src/cli/index.ts +5 -3
- 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
|
@@ -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
|
|
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.
|
|
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.
|
|
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', (
|
|
61
|
-
console.log('Capture button clicked - event:', e);
|
|
62
|
-
this.capturePhoto();
|
|
63
|
-
});
|
|
81
|
+
this.captureBtn.addEventListener('click', () => this.capturePhoto());
|
|
64
82
|
|
|
65
|
-
// Camera
|
|
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
|
-
//
|
|
74
|
-
this.
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
122
|
-
|
|
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
|
-
|
|
129
|
-
|
|
119
|
+
// Update UI classes
|
|
120
|
+
document.body.classList.toggle('mode-screen', mode === 'screen');
|
|
130
121
|
|
|
131
|
-
//
|
|
132
|
-
this.
|
|
133
|
-
this.
|
|
122
|
+
// Update mode buttons
|
|
123
|
+
this.cameraModeBtn.classList.toggle('active', mode === 'camera');
|
|
124
|
+
this.screenModeBtn.classList.toggle('active', mode === 'screen');
|
|
134
125
|
|
|
135
|
-
// Update
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
268
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
315
|
-
this.
|
|
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
|
|
320
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
this.
|
|
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
|
-
}
|
|
354
|
-
|
|
355
|
-
this.
|
|
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
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
}
|
|
384
|
-
|
|
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
|
-
|
|
388
|
-
if (
|
|
389
|
-
this.
|
|
390
|
-
|
|
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
|
-
|
|
400
|
-
this.
|
|
401
|
-
|
|
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
|
-
|
|
309
|
+
try {
|
|
310
|
+
// Optional timer countdown
|
|
311
|
+
if (this.timerToggle.checked) {
|
|
312
|
+
await this.showCountdown();
|
|
313
|
+
}
|
|
411
314
|
|
|
412
|
-
|
|
413
|
-
|
|
315
|
+
// Capture from video stream
|
|
316
|
+
const context = this.canvas.getContext('2d');
|
|
317
|
+
if (!context) return;
|
|
414
318
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
|
|
432
|
-
|
|
334
|
+
this.photos.push(photo);
|
|
335
|
+
this.addPhotoToGallery(photo);
|
|
336
|
+
this.setStatus('Photo captured!', true);
|
|
337
|
+
this.playCaptureFeedback();
|
|
433
338
|
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
|
|
458
|
-
flash.style.opacity = '0';
|
|
459
|
-
setTimeout(() => flash.remove(), 300);
|
|
373
|
+
document.body.style.backgroundColor = '';
|
|
460
374
|
}, 100);
|
|
461
375
|
}
|
|
462
376
|
|
|
463
|
-
private
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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.
|
|
408
|
+
this.modal.style.display = 'flex';
|
|
499
409
|
}
|
|
500
410
|
|
|
501
411
|
private closeModal() {
|
|
502
|
-
this.modal.
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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('
|
|
437
|
+
this.showStatus('Failed to save photo', 'error');
|
|
533
438
|
}
|
|
534
439
|
} catch (error) {
|
|
535
|
-
console.error('Error
|
|
536
|
-
this.showStatus('
|
|
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
|
-
|
|
544
|
-
|
|
448
|
+
const photoIndex = this.photos.findIndex(p => p.id === this.currentPhotoId);
|
|
449
|
+
if (photoIndex === -1) return;
|
|
545
450
|
|
|
546
|
-
//
|
|
547
|
-
this.
|
|
451
|
+
// Remove from array
|
|
452
|
+
this.photos.splice(photoIndex, 1);
|
|
548
453
|
|
|
549
|
-
//
|
|
550
|
-
this.
|
|
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(
|
|
556
|
-
this.statusText.textContent =
|
|
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.
|
|
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
|
|
580
|
-
|
|
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
|
+
});
|