capacitor-camera-multicapture 0.12.2

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 (76) hide show
  1. package/CameraMultiCapture.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +28 -0
  4. package/README.md +551 -0
  5. package/android/build.gradle +69 -0
  6. package/android/src/main/AndroidManifest.xml +6 -0
  7. package/android/src/main/java/dev/hemang/cameramulticapture/CameraConfig.java +45 -0
  8. package/android/src/main/java/dev/hemang/cameramulticapture/CameraConfigMapper.java +98 -0
  9. package/android/src/main/java/dev/hemang/cameramulticapture/CameraMultiCapture.java +10 -0
  10. package/android/src/main/java/dev/hemang/cameramulticapture/CameraMultiCapturePlugin.java +1340 -0
  11. package/android/src/main/java/dev/hemang/cameramulticapture/ExifWrapper.java +108 -0
  12. package/android/src/main/java/dev/hemang/cameramulticapture/GenericUploadWorker.java +173 -0
  13. package/android/src/main/java/dev/hemang/cameramulticapture/ImageUtils.java +215 -0
  14. package/android/src/main/java/dev/hemang/cameramulticapture/PluginVersion.java +6 -0
  15. package/android/src/main/java/dev/hemang/cameramulticapture/ThumbnailGenerator.java +48 -0
  16. package/android/src/main/res/.gitkeep +0 -0
  17. package/dist/esm/controllers/camera-controller.d.ts +127 -0
  18. package/dist/esm/controllers/camera-controller.js +388 -0
  19. package/dist/esm/controllers/camera-controller.js.map +1 -0
  20. package/dist/esm/controllers/gallery-controller.d.ts +66 -0
  21. package/dist/esm/controllers/gallery-controller.js +333 -0
  22. package/dist/esm/controllers/gallery-controller.js.map +1 -0
  23. package/dist/esm/definitions.d.ts +372 -0
  24. package/dist/esm/definitions.js +6 -0
  25. package/dist/esm/definitions.js.map +1 -0
  26. package/dist/esm/index.d.ts +5 -0
  27. package/dist/esm/index.js +6 -0
  28. package/dist/esm/index.js.map +1 -0
  29. package/dist/esm/overlay-manager.d.ts +124 -0
  30. package/dist/esm/overlay-manager.js +732 -0
  31. package/dist/esm/overlay-manager.js.map +1 -0
  32. package/dist/esm/overlay.d.ts +2 -0
  33. package/dist/esm/overlay.js +39 -0
  34. package/dist/esm/overlay.js.map +1 -0
  35. package/dist/esm/registration.d.ts +5 -0
  36. package/dist/esm/registration.js +8 -0
  37. package/dist/esm/registration.js.map +1 -0
  38. package/dist/esm/types/ui-types.d.ts +78 -0
  39. package/dist/esm/types/ui-types.js +2 -0
  40. package/dist/esm/types/ui-types.js.map +1 -0
  41. package/dist/esm/ui/capture-gesture-handler.d.ts +15 -0
  42. package/dist/esm/ui/capture-gesture-handler.js +87 -0
  43. package/dist/esm/ui/capture-gesture-handler.js.map +1 -0
  44. package/dist/esm/ui/default-styles.d.ts +27 -0
  45. package/dist/esm/ui/default-styles.js +134 -0
  46. package/dist/esm/ui/default-styles.js.map +1 -0
  47. package/dist/esm/ui/image-editor.d.ts +10 -0
  48. package/dist/esm/ui/image-editor.js +103 -0
  49. package/dist/esm/ui/image-editor.js.map +1 -0
  50. package/dist/esm/ui/layout-manager.d.ts +42 -0
  51. package/dist/esm/ui/layout-manager.js +199 -0
  52. package/dist/esm/ui/layout-manager.js.map +1 -0
  53. package/dist/esm/ui/media-viewer.d.ts +2 -0
  54. package/dist/esm/ui/media-viewer.js +267 -0
  55. package/dist/esm/ui/media-viewer.js.map +1 -0
  56. package/dist/esm/ui/pinch-zoom-handler.d.ts +37 -0
  57. package/dist/esm/ui/pinch-zoom-handler.js +118 -0
  58. package/dist/esm/ui/pinch-zoom-handler.js.map +1 -0
  59. package/dist/esm/ui/ui-factory.d.ts +39 -0
  60. package/dist/esm/ui/ui-factory.js +272 -0
  61. package/dist/esm/ui/ui-factory.js.map +1 -0
  62. package/dist/esm/version.d.ts +1 -0
  63. package/dist/esm/version.js +3 -0
  64. package/dist/esm/version.js.map +1 -0
  65. package/dist/esm/web.d.ts +69 -0
  66. package/dist/esm/web.js +122 -0
  67. package/dist/esm/web.js.map +1 -0
  68. package/dist/plugin.cjs.js +6274 -0
  69. package/dist/plugin.cjs.js.map +1 -0
  70. package/dist/plugin.js +6276 -0
  71. package/dist/plugin.js.map +1 -0
  72. package/ios/Sources/CameraMultiCapturePlugin/CameraMultiCapture.swift +8 -0
  73. package/ios/Sources/CameraMultiCapturePlugin/CameraMultiCapturePlugin.swift +1551 -0
  74. package/ios/Sources/CameraMultiCapturePlugin/PluginVersion.swift +2 -0
  75. package/ios/Tests/CameraMultiCapturePluginTests/CameraMultiCapturePluginTests.swift +15 -0
  76. package/package.json +93 -0
@@ -0,0 +1,732 @@
1
+ /**
2
+ * Overlay Manager - Main controller for camera overlay UI
3
+ */
4
+ import { TorchState } from './definitions';
5
+ import { CameraController } from './controllers/camera-controller';
6
+ import { GalleryController } from './controllers/gallery-controller';
7
+ import { merge } from 'lodash';
8
+ import { defaultButtons } from './ui/default-styles';
9
+ import { createButton, createShotCounter, updateShotCounter, setButtonIcon } from './ui/ui-factory';
10
+ import { createOverlayContainer, createPositionContainers, createBottomGridCells, createGallery, } from './ui/layout-manager';
11
+ import { PinchZoomHandler } from './ui/pinch-zoom-handler';
12
+ import { bindCaptureGestures } from './ui/capture-gesture-handler';
13
+ /**
14
+ * Main class to manage camera overlay UI
15
+ */
16
+ export class OverlayManager {
17
+ constructor(plugin, options) {
18
+ this.overlayElement = null;
19
+ this.galleryController = null;
20
+ this.isActive = false;
21
+ this.resolvePromise = null;
22
+ this.bodyBackgroundColor = null;
23
+ this.zoomContainer = null;
24
+ this.zoomConfig = null;
25
+ this.zoomButtonsList = [];
26
+ this.zoomButtonLevels = [];
27
+ this.shotCounter = null;
28
+ this.shotCount = 0;
29
+ this.torchButton = null;
30
+ this.torchConfig = null;
31
+ this.pinchHandler = null;
32
+ this.isRecordingVideo = false;
33
+ this.captureButton = null;
34
+ this.captureGestureBinding = null;
35
+ this.recordingIndicator = null;
36
+ this.recordingTimerText = null;
37
+ this.recordingStartedAt = 0;
38
+ this.recordingIntervalId = null;
39
+ this.boundOrientationHandler = null;
40
+ this.options = options;
41
+ this.cameraController = new CameraController(plugin, options);
42
+ }
43
+ /**
44
+ * Emits photoAdded event using pure JavaScript events
45
+ */
46
+ emitPhotoAddedEvent(eventData) {
47
+ try {
48
+ const event = new CustomEvent('photoAdded', { detail: eventData });
49
+ window.dispatchEvent(event);
50
+ }
51
+ catch (error) {
52
+ console.error('[CameraMultiCapture] Failed to emit photoAdded event:', error);
53
+ }
54
+ }
55
+ /**
56
+ * Emits photoUpdated event using pure JavaScript events
57
+ */
58
+ emitPhotoUpdatedEvent(eventData) {
59
+ try {
60
+ const event = new CustomEvent('photoUpdated', { detail: eventData });
61
+ window.dispatchEvent(event);
62
+ }
63
+ catch (error) {
64
+ console.error('[CameraMultiCapture] Failed to emit photoUpdated event:', error);
65
+ }
66
+ }
67
+ /**
68
+ * Emits photoRemoved event using pure JavaScript events
69
+ */
70
+ emitPhotoRemovedEvent(eventData) {
71
+ try {
72
+ const event = new CustomEvent('photoRemoved', { detail: eventData });
73
+ window.dispatchEvent(event);
74
+ }
75
+ catch (error) {
76
+ console.error('[CameraMultiCapture] Failed to emit photoRemoved event:', error);
77
+ }
78
+ }
79
+ /**
80
+ * Emits videoRecordingStarted event using pure JavaScript events.
81
+ */
82
+ emitVideoRecordingStartedEvent(eventData) {
83
+ try {
84
+ const event = new CustomEvent('videoRecordingStarted', { detail: eventData });
85
+ window.dispatchEvent(event);
86
+ }
87
+ catch (error) {
88
+ console.error('[CameraMultiCapture] Failed to emit videoRecordingStarted event:', error);
89
+ }
90
+ }
91
+ /**
92
+ * Emits videoRecordingStopped event using pure JavaScript events.
93
+ */
94
+ emitVideoRecordingStoppedEvent(eventData) {
95
+ try {
96
+ const event = new CustomEvent('videoRecordingStopped', { detail: eventData });
97
+ window.dispatchEvent(event);
98
+ }
99
+ catch (error) {
100
+ console.error('[CameraMultiCapture] Failed to emit videoRecordingStopped event:', error);
101
+ }
102
+ }
103
+ /**
104
+ * Emits videoAdded event using pure JavaScript events.
105
+ */
106
+ emitVideoAddedEvent(eventData) {
107
+ try {
108
+ const event = new CustomEvent('videoAdded', { detail: eventData });
109
+ window.dispatchEvent(event);
110
+ }
111
+ catch (error) {
112
+ console.error('[CameraMultiCapture] Failed to emit videoAdded event:', error);
113
+ }
114
+ }
115
+ /**
116
+ * Emits videoRemoved event using pure JavaScript events.
117
+ */
118
+ emitVideoRemovedEvent(eventData) {
119
+ try {
120
+ const event = new CustomEvent('videoRemoved', { detail: eventData });
121
+ window.dispatchEvent(event);
122
+ }
123
+ catch (error) {
124
+ console.error('[CameraMultiCapture] Failed to emit videoRemoved event:', error);
125
+ }
126
+ }
127
+ /**
128
+ * Shows the camera overlay and returns captured images
129
+ */
130
+ async setup() {
131
+ // Only allow one active overlay at a time
132
+ if (this.isActive) {
133
+ throw new Error('Camera overlay is already active');
134
+ }
135
+ this.isActive = true;
136
+ return new Promise(async (resolve) => {
137
+ var _a, _b;
138
+ this.resolvePromise = resolve;
139
+ try {
140
+ this.createOverlayUI();
141
+ const container = document.getElementById(this.options.containerId);
142
+ if (!container) {
143
+ throw new Error(`Container with ID ${this.options.containerId} not found`);
144
+ }
145
+ this.bodyBackgroundColor = document.body.style.backgroundColor;
146
+ document.body.style.backgroundColor = 'transparent';
147
+ await this.cameraController.initialize(container, (_a = this.options.quality) !== null && _a !== void 0 ? _a : 90);
148
+ // Create zoom buttons after camera init
149
+ if (this.zoomContainer && this.zoomConfig) {
150
+ await this.createZoomButtonsAfterInit();
151
+ }
152
+ // JavaScript pinch-to-zoom when enabled
153
+ if ((_b = this.options.pinchToZoom) === null || _b === void 0 ? void 0 : _b.enabled) {
154
+ this.pinchHandler = new PinchZoomHandler(this.cameraController, { options: this.options }, (zoom) => this.updateZoomButtonSelection(zoom));
155
+ await this.pinchHandler.attach(container);
156
+ }
157
+ }
158
+ catch (error) {
159
+ console.error('Failed to initialize camera overlay', error);
160
+ resolve({ images: [], videos: [], cancelled: true });
161
+ this.cleanup();
162
+ }
163
+ });
164
+ }
165
+ async refresh() {
166
+ await this.cameraController.refresh();
167
+ }
168
+ handleOrientationChange() {
169
+ this.refresh();
170
+ }
171
+ /**
172
+ * Creates the overlay UI
173
+ */
174
+ createOverlayUI() {
175
+ var _a;
176
+ const container = document.getElementById(this.options.containerId);
177
+ if (!container) {
178
+ throw new Error(`Container with ID ${this.options.containerId} not found`);
179
+ }
180
+ // Create overlay container
181
+ this.overlayElement = createOverlayContainer(this.options.containerId);
182
+ // Create position containers
183
+ const positions = createPositionContainers(this.overlayElement);
184
+ // Create bottom grid cells
185
+ const bottomCells = createBottomGridCells(positions.bottomGrid);
186
+ const galleryElement = createGallery(this.overlayElement);
187
+ if (this.options.maxCaptures === 1) {
188
+ galleryElement.style.display = 'none';
189
+ }
190
+ this.galleryController = new GalleryController(galleryElement, this.options.thumbnailStyle, undefined, (eventData) => {
191
+ this.emitPhotoAddedEvent(eventData);
192
+ }, (eventData) => {
193
+ this.emitPhotoUpdatedEvent(eventData);
194
+ }, (eventData) => {
195
+ this.emitPhotoRemovedEvent(eventData);
196
+ }, (eventData) => {
197
+ this.emitVideoAddedEvent(eventData);
198
+ }, (eventData) => {
199
+ this.emitVideoRemovedEvent(eventData);
200
+ }, (totalCount) => {
201
+ if (this.options.showShotCounter) {
202
+ this.shotCount = totalCount;
203
+ if (this.shotCounter) {
204
+ updateShotCounter(this.shotCounter, this.shotCount);
205
+ }
206
+ }
207
+ }, typeof this.options.enableEditing === 'object' ? true : ((_a = this.options.enableEditing) !== null && _a !== void 0 ? _a : false), typeof this.options.enableEditing === 'object' ? this.options.enableEditing.markerJsLicenseKey : undefined);
208
+ // Merge default buttons with user-provided options
209
+ const buttons = merge(defaultButtons, this.options.buttons || {});
210
+ // Create buttons
211
+ const captureBtn = createButton(buttons.capture);
212
+ this.captureButton = captureBtn;
213
+ // Create shot counter only if enabled
214
+ if (this.options.showShotCounter) {
215
+ this.shotCounter = createShotCounter();
216
+ }
217
+ // Place capture button back in center (no container needed)
218
+ bottomCells.middle.appendChild(captureBtn);
219
+ this.ensureRecordingGlowStyles();
220
+ this.recordingIndicator = this.createRecordingIndicator();
221
+ Object.assign(this.recordingIndicator.style, {
222
+ position: 'absolute',
223
+ top: '12px',
224
+ left: '50%',
225
+ transform: 'translateX(-50%)',
226
+ });
227
+ this.overlayElement.appendChild(this.recordingIndicator);
228
+ this.captureGestureBinding = bindCaptureGestures(captureBtn, {
229
+ onTap: () => this.handleCaptureTap(),
230
+ onHoldStart: () => this.handleCaptureHoldStart(),
231
+ onHoldEnd: () => this.handleCaptureRelease(),
232
+ });
233
+ // Only show Done button if not in single capture mode
234
+ if (this.options.maxCaptures !== 1) {
235
+ const doneBtn = createButton(buttons.done);
236
+ // If counter is enabled, create a container with counter and done button
237
+ if (this.shotCounter) {
238
+ const rightContainer = document.createElement('div');
239
+ Object.assign(rightContainer.style, {
240
+ display: 'flex',
241
+ alignItems: 'center',
242
+ justifyContent: 'center',
243
+ gap: '0.625rem',
244
+ });
245
+ rightContainer.appendChild(this.shotCounter);
246
+ rightContainer.appendChild(doneBtn);
247
+ bottomCells.right.appendChild(rightContainer);
248
+ }
249
+ else {
250
+ // No counter, just add done button directly
251
+ bottomCells.right.appendChild(doneBtn);
252
+ }
253
+ doneBtn.onclick = () => {
254
+ this.completeCapture(false);
255
+ };
256
+ }
257
+ else {
258
+ // Single capture mode - if counter is enabled, show it in the right cell
259
+ if (this.shotCounter) {
260
+ bottomCells.right.appendChild(this.shotCounter);
261
+ }
262
+ }
263
+ const cancelBtn = createButton(buttons.cancel);
264
+ bottomCells.left.appendChild(cancelBtn);
265
+ cancelBtn.onclick = () => {
266
+ this.completeCapture(true);
267
+ };
268
+ if (buttons.switchCamera) {
269
+ this.createSwitchCameraButton(buttons.switchCamera, positions.topRight);
270
+ }
271
+ if (buttons.torch) {
272
+ this.createTorchButton(buttons.torch, positions.topRight);
273
+ }
274
+ if (buttons.flash) {
275
+ this.createFlashButton(buttons.flash, positions.topLeft);
276
+ }
277
+ if (buttons.zoom) {
278
+ // Store zoom config and container for later creation
279
+ this.zoomConfig = buttons.zoom;
280
+ this.zoomContainer = positions.zoomRow;
281
+ }
282
+ this.boundOrientationHandler = () => this.handleOrientationChange();
283
+ window.addEventListener('orientationchange', this.boundOrientationHandler);
284
+ }
285
+ /**
286
+ * Creates the switch camera button
287
+ */
288
+ createSwitchCameraButton(config, container) {
289
+ const switchBtn = createButton(config);
290
+ switchBtn.onclick = async () => {
291
+ try {
292
+ await this.cameraController.switchCamera();
293
+ await this.refreshTorchIconFromState();
294
+ }
295
+ catch (error) {
296
+ console.error('Failed to switch camera', error);
297
+ }
298
+ };
299
+ container.appendChild(switchBtn);
300
+ }
301
+ /**
302
+ * Creates the torch (flashlight) toggle button, stacked below the switch camera button.
303
+ */
304
+ createTorchButton(config, container) {
305
+ const torchBtn = createButton(Object.assign(Object.assign({}, config), { icon: config.offIcon }));
306
+ this.torchButton = torchBtn;
307
+ this.torchConfig = config;
308
+ const updateTorchIcon = async (state) => {
309
+ const icon = state === TorchState.On ? config.onIcon : config.offIcon;
310
+ if (icon) {
311
+ await setButtonIcon(torchBtn, icon);
312
+ }
313
+ };
314
+ torchBtn.onclick = async () => {
315
+ try {
316
+ const state = await this.cameraController.toggleTorch();
317
+ await updateTorchIcon(state);
318
+ }
319
+ catch (error) {
320
+ console.error('Failed to toggle torch', error);
321
+ }
322
+ };
323
+ // Best-effort initial state sync; defaults to off if call fails.
324
+ this.cameraController.getTorch()
325
+ .then((state) => updateTorchIcon(state))
326
+ .catch(() => { });
327
+ container.appendChild(torchBtn);
328
+ }
329
+ /**
330
+ * Refreshes the torch icon based on current native torch state.
331
+ */
332
+ async refreshTorchIconFromState() {
333
+ if (!this.torchButton || !this.torchConfig)
334
+ return;
335
+ try {
336
+ const state = await this.cameraController.getTorch();
337
+ const icon = state === TorchState.On ? this.torchConfig.onIcon : this.torchConfig.offIcon;
338
+ if (icon) {
339
+ await setButtonIcon(this.torchButton, icon);
340
+ }
341
+ }
342
+ catch (_a) {
343
+ // state sync failure is non-blocking; keep current icon
344
+ }
345
+ }
346
+ /**
347
+ * Creates zoom buttons after camera initialization
348
+ */
349
+ async createZoomButtonsAfterInit() {
350
+ if (!this.zoomContainer || !this.zoomConfig)
351
+ return;
352
+ let smartZoomLevels;
353
+ try {
354
+ // Get smart zoom levels that include physical camera switches
355
+ smartZoomLevels = await this.cameraController.getSmartZoomLevels();
356
+ }
357
+ catch (error) {
358
+ // Use fallback zoom levels
359
+ const fallbackLevels = this.zoomConfig.levels || [1, 2, 3, 4];
360
+ smartZoomLevels = fallbackLevels.map((level) => ({ level, isPhysicalCamera: false }));
361
+ }
362
+ // Create zoom buttons with the smart levels
363
+ this.createZoomButtons(smartZoomLevels, this.zoomContainer);
364
+ }
365
+ /**
366
+ * Creates smart zoom buttons with physical camera indication
367
+ */
368
+ createZoomButtons(levels, container) {
369
+ const config = this.zoomConfig || {};
370
+ // Track buttons and their corresponding zoom levels so we can update them from pinch and clicks
371
+ this.zoomButtonsList = [];
372
+ this.zoomButtonLevels = levels.map((z) => z.level);
373
+ let currentZoomLevel = 1; // Default zoom level
374
+ // Add zoom buttons in a horizontal row
375
+ levels.forEach((zoomInfo) => {
376
+ const level = zoomInfo.level;
377
+ // Format zoom level display
378
+ let displayText;
379
+ if (level === 1) {
380
+ // Special case for 1x - always show as "1x"
381
+ displayText = '1x';
382
+ }
383
+ else if (level % 1 === 0) {
384
+ // Show as integer for whole numbers (2x, 3x, etc.)
385
+ displayText = `${Math.round(level)}x`;
386
+ }
387
+ else {
388
+ // Round to one decimal place for fractional numbers (0.673434 -> 0.7x)
389
+ displayText = `${Math.round(level * 10) / 10}x`;
390
+ }
391
+ // Create a button for each zoom level
392
+ const zoomBtn = createButton(Object.assign(Object.assign({}, config), { text: displayText }));
393
+ // Make zoom buttons smaller and more compact
394
+ Object.assign(zoomBtn.style, {
395
+ padding: '5px 8px',
396
+ minWidth: '40px',
397
+ minHeight: '30px',
398
+ margin: '0 3px',
399
+ fontSize: '14px',
400
+ fontWeight: '500',
401
+ transition: 'all 0.2s ease',
402
+ });
403
+ zoomBtn.onclick = async () => {
404
+ try {
405
+ // Use smart zoom to handle physical camera switching
406
+ await this.cameraController.performSmartZoom(level);
407
+ currentZoomLevel = level;
408
+ // Update button states based on the new zoom level
409
+ this.updateZoomButtonSelection(currentZoomLevel);
410
+ }
411
+ catch (error) {
412
+ console.error(`Failed to set zoom to ${level}x`, error);
413
+ }
414
+ };
415
+ this.zoomButtonsList.push(zoomBtn);
416
+ container.appendChild(zoomBtn);
417
+ });
418
+ // Initial highlight (typically 1x)
419
+ this.updateZoomButtonSelection(currentZoomLevel);
420
+ }
421
+ /**
422
+ * Updates zoom button styles so the button closest to currentZoom is highlighted.
423
+ */
424
+ updateZoomButtonSelection(currentZoom) {
425
+ if (!this.zoomButtonsList.length || !this.zoomButtonLevels.length)
426
+ return;
427
+ // Find the zoom level closest to the current zoom
428
+ let nearestIdx = 0;
429
+ let bestDist = Math.abs(this.zoomButtonLevels[0] - currentZoom);
430
+ for (let i = 1; i < this.zoomButtonLevels.length; i++) {
431
+ const d = Math.abs(this.zoomButtonLevels[i] - currentZoom);
432
+ if (d < bestDist) {
433
+ bestDist = d;
434
+ nearestIdx = i;
435
+ }
436
+ }
437
+ // Highlight only the nearest button
438
+ this.zoomButtonsList.forEach((btn, idx) => {
439
+ if (idx === nearestIdx) {
440
+ Object.assign(btn.style, {
441
+ backgroundColor: '#ffffff',
442
+ color: '#000000',
443
+ fontWeight: '700',
444
+ });
445
+ }
446
+ else {
447
+ Object.assign(btn.style, {
448
+ backgroundColor: 'rgba(0,0,0,0.5)',
449
+ color: '#ffffff',
450
+ fontWeight: '500',
451
+ });
452
+ }
453
+ });
454
+ }
455
+ /**
456
+ * Creates the flash toggle button
457
+ */
458
+ createFlashButton(config, container) {
459
+ const flashBtn = createButton(Object.assign(Object.assign({}, config), { icon: config.offIcon // Start with off icon
460
+ }));
461
+ const updateFlashIcon = async (mode) => {
462
+ let icon;
463
+ switch (mode) {
464
+ case 'on':
465
+ icon = config.onIcon;
466
+ break;
467
+ case 'auto':
468
+ // Only show auto icon if auto mode is enabled, otherwise show off icon
469
+ icon = this.cameraController.isFlashAutoModeEnabled() ? config.autoIcon : config.offIcon;
470
+ break;
471
+ case 'off':
472
+ default:
473
+ icon = config.offIcon;
474
+ break;
475
+ }
476
+ // Only update icon if one is provided
477
+ if (icon) {
478
+ await setButtonIcon(flashBtn, icon);
479
+ }
480
+ };
481
+ flashBtn.onclick = async () => {
482
+ try {
483
+ const newMode = await this.cameraController.toggleFlash();
484
+ await updateFlashIcon(newMode);
485
+ }
486
+ catch (error) {
487
+ console.error('Failed to toggle flash', error);
488
+ }
489
+ };
490
+ container.appendChild(flashBtn);
491
+ }
492
+ /**
493
+ * Handles quick tap capture for still images.
494
+ */
495
+ async handleCaptureTap() {
496
+ try {
497
+ // Turn off torch before capture when flash is enabled to prevent LED conflict
498
+ const flashMode = this.cameraController.getFlashMode();
499
+ if (flashMode === 'on' || flashMode === 'auto') {
500
+ const torchState = await this.cameraController.getTorch();
501
+ if (torchState === TorchState.On) {
502
+ await this.cameraController.setTorch(TorchState.Off);
503
+ await this.refreshTorchIconFromState();
504
+ }
505
+ }
506
+ const imageData = await this.cameraController.captureImage();
507
+ if (!imageData || !this.galleryController)
508
+ return;
509
+ this.galleryController.addImage(imageData);
510
+ if (this.options.maxCaptures && this.galleryController.getImages().length >= this.options.maxCaptures) {
511
+ setTimeout(() => this.completeCapture(false), 100);
512
+ }
513
+ }
514
+ catch (error) {
515
+ console.error('Failed to capture image', error);
516
+ }
517
+ }
518
+ /**
519
+ * Handles long press start to begin video recording.
520
+ */
521
+ async handleCaptureHoldStart() {
522
+ if (this.isRecordingVideo || this.cameraController.getRecordingState()) {
523
+ return;
524
+ }
525
+ try {
526
+ await this.cameraController.startVideoRecording();
527
+ this.isRecordingVideo = true;
528
+ this.recordingStartedAt = Date.now();
529
+ this.startRecordingTimer();
530
+ this.showRecordingIndicator(true);
531
+ this.setCaptureGlow(true);
532
+ this.emitVideoRecordingStartedEvent({ timestamp: this.recordingStartedAt });
533
+ }
534
+ catch (error) {
535
+ this.isRecordingVideo = false;
536
+ console.error('Failed to start video recording', error);
537
+ }
538
+ }
539
+ /**
540
+ * Handles pointer release for either tap photo or stop video recording.
541
+ */
542
+ async handleCaptureRelease() {
543
+ if (!this.isRecordingVideo) {
544
+ return;
545
+ }
546
+ try {
547
+ const videoData = await this.cameraController.stopVideoRecording();
548
+ this.isRecordingVideo = false;
549
+ this.stopRecordingTimer();
550
+ this.showRecordingIndicator(false);
551
+ this.setCaptureGlow(false);
552
+ if (videoData && this.galleryController) {
553
+ this.galleryController.addVideo(videoData);
554
+ this.emitVideoRecordingStoppedEvent({
555
+ video: videoData,
556
+ totalCount: this.galleryController.getVideos().length,
557
+ });
558
+ }
559
+ await this.refreshTorchIconFromState();
560
+ }
561
+ catch (error) {
562
+ this.isRecordingVideo = false;
563
+ this.stopRecordingTimer();
564
+ this.showRecordingIndicator(false);
565
+ this.setCaptureGlow(false);
566
+ console.error('Failed to stop video recording', error);
567
+ }
568
+ }
569
+ ensureRecordingGlowStyles() {
570
+ const id = 'cmmc-recording-glow-styles';
571
+ if (document.getElementById(id))
572
+ return;
573
+ const style = document.createElement('style');
574
+ style.id = id;
575
+ style.textContent = `
576
+ @keyframes cmmcCaptureGlow {
577
+ 0% { filter: drop-shadow(0 0 6px rgba(255, 59, 48, 0.7)); }
578
+ 50% { filter: drop-shadow(0 0 18px rgba(255, 59, 48, 1)) drop-shadow(0 0 40px rgba(255, 59, 48, 0.5)); }
579
+ 100% { filter: drop-shadow(0 0 6px rgba(255, 59, 48, 0.7)); }
580
+ }
581
+ .cmmc-capture-recording {
582
+ animation: cmmcCaptureGlow 1.2s ease-in-out infinite !important;
583
+ filter: drop-shadow(0 0 6px rgba(255, 59, 48, 0.7)) !important;
584
+ }
585
+ `;
586
+ document.head.appendChild(style);
587
+ }
588
+ setCaptureGlow(active) {
589
+ if (!this.captureButton)
590
+ return;
591
+ if (active) {
592
+ this.captureButton.classList.add('cmmc-capture-recording');
593
+ }
594
+ else {
595
+ this.captureButton.classList.remove('cmmc-capture-recording');
596
+ }
597
+ }
598
+ createRecordingIndicator() {
599
+ const indicator = document.createElement('div');
600
+ const dot = document.createElement('span');
601
+ const timer = document.createElement('span');
602
+ timer.textContent = '00:00';
603
+ Object.assign(indicator.style, {
604
+ display: 'none',
605
+ alignItems: 'center',
606
+ gap: '6px',
607
+ padding: '6px 10px',
608
+ borderRadius: '14px',
609
+ background: 'rgba(0, 0, 0, 0.55)',
610
+ color: '#ffffff',
611
+ fontSize: '12px',
612
+ fontWeight: '600',
613
+ pointerEvents: 'none',
614
+ zIndex: '4',
615
+ });
616
+ Object.assign(dot.style, {
617
+ width: '8px',
618
+ height: '8px',
619
+ borderRadius: '50%',
620
+ background: '#ff3b30',
621
+ boxShadow: '0 0 10px rgba(255, 59, 48, 0.9)',
622
+ animation: 'cmmcRecordPulse 1s infinite',
623
+ });
624
+ const styleTag = document.createElement('style');
625
+ styleTag.textContent = `
626
+ @keyframes cmmcRecordPulse {
627
+ 0% { opacity: 1; transform: scale(1); }
628
+ 50% { opacity: 0.35; transform: scale(0.9); }
629
+ 100% { opacity: 1; transform: scale(1); }
630
+ }
631
+ `;
632
+ indicator.appendChild(styleTag);
633
+ indicator.appendChild(dot);
634
+ indicator.appendChild(timer);
635
+ this.recordingTimerText = timer;
636
+ return indicator;
637
+ }
638
+ showRecordingIndicator(visible) {
639
+ if (!this.recordingIndicator)
640
+ return;
641
+ this.recordingIndicator.style.display = visible ? 'flex' : 'none';
642
+ if (!visible && this.recordingTimerText) {
643
+ this.recordingTimerText.textContent = '00:00';
644
+ }
645
+ }
646
+ startRecordingTimer() {
647
+ this.stopRecordingTimer();
648
+ this.recordingIntervalId = window.setInterval(() => {
649
+ if (!this.recordingTimerText)
650
+ return;
651
+ const elapsedSeconds = Math.floor((Date.now() - this.recordingStartedAt) / 1000);
652
+ if (this.options.maxRecordingDuration && elapsedSeconds >= this.options.maxRecordingDuration) {
653
+ // Prevent duplicate stop calls while awaiting native stop completion.
654
+ this.stopRecordingTimer();
655
+ void this.handleCaptureRelease();
656
+ return;
657
+ }
658
+ const minutes = Math.floor(elapsedSeconds / 60);
659
+ const seconds = elapsedSeconds % 60;
660
+ this.recordingTimerText.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
661
+ }, 200);
662
+ }
663
+ stopRecordingTimer() {
664
+ if (this.recordingIntervalId !== null) {
665
+ clearInterval(this.recordingIntervalId);
666
+ this.recordingIntervalId = null;
667
+ }
668
+ }
669
+ /**
670
+ * Completes the capture process
671
+ */
672
+ completeCapture(cancelled) {
673
+ var _a, _b;
674
+ const images = ((_a = this.galleryController) === null || _a === void 0 ? void 0 : _a.getImages()) || [];
675
+ const videos = ((_b = this.galleryController) === null || _b === void 0 ? void 0 : _b.getVideos()) || [];
676
+ this.cleanup();
677
+ if (this.resolvePromise) {
678
+ this.resolvePromise({
679
+ images: !cancelled ? images.map(img => img.data) : [],
680
+ videos: !cancelled ? videos.map(vid => vid.data) : [],
681
+ cancelled
682
+ });
683
+ this.resolvePromise = null;
684
+ }
685
+ }
686
+ /**
687
+ * Cleans up resources
688
+ */
689
+ cleanup() {
690
+ if (this.captureGestureBinding) {
691
+ this.captureGestureBinding.detach();
692
+ this.captureGestureBinding = null;
693
+ }
694
+ if (this.isRecordingVideo) {
695
+ this.cameraController.stopVideoRecording().catch(() => {
696
+ // best effort stop during cleanup
697
+ });
698
+ this.isRecordingVideo = false;
699
+ }
700
+ this.stopRecordingTimer();
701
+ this.showRecordingIndicator(false);
702
+ this.setCaptureGlow(false);
703
+ this.recordingIndicator = null;
704
+ this.recordingTimerText = null;
705
+ this.captureButton = null;
706
+ this.torchButton = null;
707
+ this.torchConfig = null;
708
+ if (this.pinchHandler) {
709
+ this.pinchHandler.detach();
710
+ this.pinchHandler = null;
711
+ }
712
+ this.cameraController.stop().catch((err) => {
713
+ console.warn('Error stopping camera', err);
714
+ });
715
+ if (this.overlayElement && this.overlayElement.parentElement) {
716
+ this.overlayElement.parentElement.removeChild(this.overlayElement);
717
+ }
718
+ this.overlayElement = null;
719
+ this.galleryController = null;
720
+ this.isActive = false;
721
+ this.shotCounter = null;
722
+ this.shotCount = 0; // Reset shot count for next session
723
+ if (this.bodyBackgroundColor) {
724
+ document.body.style.backgroundColor = this.bodyBackgroundColor;
725
+ }
726
+ if (this.boundOrientationHandler) {
727
+ window.removeEventListener('orientationchange', this.boundOrientationHandler);
728
+ this.boundOrientationHandler = null;
729
+ }
730
+ }
731
+ }
732
+ //# sourceMappingURL=overlay-manager.js.map