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.
- package/CameraMultiCapture.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +551 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraConfig.java +45 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraConfigMapper.java +98 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraMultiCapture.java +10 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraMultiCapturePlugin.java +1340 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ExifWrapper.java +108 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/GenericUploadWorker.java +173 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ImageUtils.java +215 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/PluginVersion.java +6 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ThumbnailGenerator.java +48 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/esm/controllers/camera-controller.d.ts +127 -0
- package/dist/esm/controllers/camera-controller.js +388 -0
- package/dist/esm/controllers/camera-controller.js.map +1 -0
- package/dist/esm/controllers/gallery-controller.d.ts +66 -0
- package/dist/esm/controllers/gallery-controller.js +333 -0
- package/dist/esm/controllers/gallery-controller.js.map +1 -0
- package/dist/esm/definitions.d.ts +372 -0
- package/dist/esm/definitions.js +6 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/overlay-manager.d.ts +124 -0
- package/dist/esm/overlay-manager.js +732 -0
- package/dist/esm/overlay-manager.js.map +1 -0
- package/dist/esm/overlay.d.ts +2 -0
- package/dist/esm/overlay.js +39 -0
- package/dist/esm/overlay.js.map +1 -0
- package/dist/esm/registration.d.ts +5 -0
- package/dist/esm/registration.js +8 -0
- package/dist/esm/registration.js.map +1 -0
- package/dist/esm/types/ui-types.d.ts +78 -0
- package/dist/esm/types/ui-types.js +2 -0
- package/dist/esm/types/ui-types.js.map +1 -0
- package/dist/esm/ui/capture-gesture-handler.d.ts +15 -0
- package/dist/esm/ui/capture-gesture-handler.js +87 -0
- package/dist/esm/ui/capture-gesture-handler.js.map +1 -0
- package/dist/esm/ui/default-styles.d.ts +27 -0
- package/dist/esm/ui/default-styles.js +134 -0
- package/dist/esm/ui/default-styles.js.map +1 -0
- package/dist/esm/ui/image-editor.d.ts +10 -0
- package/dist/esm/ui/image-editor.js +103 -0
- package/dist/esm/ui/image-editor.js.map +1 -0
- package/dist/esm/ui/layout-manager.d.ts +42 -0
- package/dist/esm/ui/layout-manager.js +199 -0
- package/dist/esm/ui/layout-manager.js.map +1 -0
- package/dist/esm/ui/media-viewer.d.ts +2 -0
- package/dist/esm/ui/media-viewer.js +267 -0
- package/dist/esm/ui/media-viewer.js.map +1 -0
- package/dist/esm/ui/pinch-zoom-handler.d.ts +37 -0
- package/dist/esm/ui/pinch-zoom-handler.js +118 -0
- package/dist/esm/ui/pinch-zoom-handler.js.map +1 -0
- package/dist/esm/ui/ui-factory.d.ts +39 -0
- package/dist/esm/ui/ui-factory.js +272 -0
- package/dist/esm/ui/ui-factory.js.map +1 -0
- package/dist/esm/version.d.ts +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/esm/web.d.ts +69 -0
- package/dist/esm/web.js +122 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +6274 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +6276 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/CameraMultiCapturePlugin/CameraMultiCapture.swift +8 -0
- package/ios/Sources/CameraMultiCapturePlugin/CameraMultiCapturePlugin.swift +1551 -0
- package/ios/Sources/CameraMultiCapturePlugin/PluginVersion.swift +2 -0
- package/ios/Tests/CameraMultiCapturePluginTests/CameraMultiCapturePluginTests.swift +15 -0
- 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
|