@smartbit4all/ng-client 7.3.42 → 7.3.43
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.
|
@@ -9185,9 +9185,11 @@ class UploadWidgetUtils {
|
|
|
9185
9185
|
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(1));
|
|
9186
9186
|
return `${value} ${sizes[i]}`;
|
|
9187
9187
|
}
|
|
9188
|
-
/** The size of a capture
|
|
9189
|
-
|
|
9190
|
-
|
|
9188
|
+
/** The size of a capture for the Capture Dialog's captions: a decimal point in English, a
|
|
9189
|
+
* decimal comma in Hungarian and without a known locale. */
|
|
9190
|
+
static formatCapturedSize(bytes, locale) {
|
|
9191
|
+
const size = UploadWidgetUtils.formatSize(bytes);
|
|
9192
|
+
return locale?.startsWith('en') ? size : size.replace('.', ',');
|
|
9191
9193
|
}
|
|
9192
9194
|
/** A length of time as mm:ss. */
|
|
9193
9195
|
static formatDuration(milliseconds) {
|
|
@@ -9206,6 +9208,89 @@ class UploadWidgetUtils {
|
|
|
9206
9208
|
const ext = mimeType.includes('webm') ? 'webm' : 'mp4';
|
|
9207
9209
|
return new File([blob], `${prefix}_${formattedDate}.${ext}`, { type: mimeType });
|
|
9208
9210
|
}
|
|
9211
|
+
/** An image re-encoded as a jpeg, smaller step by step until it fits; the file itself when it
|
|
9212
|
+
* does not fit at the floor of the quality and the size, or is no image. */
|
|
9213
|
+
static async compressImageFile(file, maxFileSize) {
|
|
9214
|
+
if (!file.type.startsWith('image/'))
|
|
9215
|
+
return file;
|
|
9216
|
+
const img = await new Promise((resolve, reject) => {
|
|
9217
|
+
const url = URL.createObjectURL(file);
|
|
9218
|
+
const image = new Image();
|
|
9219
|
+
image.onload = () => {
|
|
9220
|
+
URL.revokeObjectURL(url);
|
|
9221
|
+
resolve(image);
|
|
9222
|
+
};
|
|
9223
|
+
image.onerror = (e) => {
|
|
9224
|
+
URL.revokeObjectURL(url);
|
|
9225
|
+
reject(e);
|
|
9226
|
+
};
|
|
9227
|
+
image.src = url;
|
|
9228
|
+
});
|
|
9229
|
+
const canvas = document.createElement('canvas');
|
|
9230
|
+
const ctx = canvas.getContext('2d');
|
|
9231
|
+
if (!ctx)
|
|
9232
|
+
return file;
|
|
9233
|
+
let width = img.naturalWidth;
|
|
9234
|
+
let height = img.naturalHeight;
|
|
9235
|
+
let quality = 0.9;
|
|
9236
|
+
const minQuality = 0.4;
|
|
9237
|
+
const minDimension = 640;
|
|
9238
|
+
const getBlob = (q) => new Promise((resolve) => {
|
|
9239
|
+
canvas.width = width;
|
|
9240
|
+
canvas.height = height;
|
|
9241
|
+
ctx.clearRect(0, 0, width, height);
|
|
9242
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
9243
|
+
canvas.toBlob((blob) => {
|
|
9244
|
+
if (blob)
|
|
9245
|
+
resolve(blob);
|
|
9246
|
+
}, 'image/jpeg', q);
|
|
9247
|
+
});
|
|
9248
|
+
let blob = await getBlob(quality);
|
|
9249
|
+
while (blob.size > maxFileSize) {
|
|
9250
|
+
if (quality > minQuality) {
|
|
9251
|
+
quality -= 0.1;
|
|
9252
|
+
}
|
|
9253
|
+
else if (width > minDimension && height > minDimension) {
|
|
9254
|
+
width = Math.floor(width * 0.8);
|
|
9255
|
+
height = Math.floor(height * 0.8);
|
|
9256
|
+
quality = 0.9;
|
|
9257
|
+
}
|
|
9258
|
+
else {
|
|
9259
|
+
break;
|
|
9260
|
+
}
|
|
9261
|
+
blob = await getBlob(quality);
|
|
9262
|
+
}
|
|
9263
|
+
if (blob.size <= maxFileSize) {
|
|
9264
|
+
return new File([blob], file.name, { type: 'image/jpeg' });
|
|
9265
|
+
}
|
|
9266
|
+
else {
|
|
9267
|
+
return file;
|
|
9268
|
+
}
|
|
9269
|
+
}
|
|
9270
|
+
/** The descriptor of a capture button: raised, in the primary colour unless told otherwise. */
|
|
9271
|
+
static createCaptureButton(title, icon, color = 'primary', type = UiActionButtonType.RAISED) {
|
|
9272
|
+
return { type, title, icon, iconPosition: IconPosition.PRE, color };
|
|
9273
|
+
}
|
|
9274
|
+
/** The button that takes a picture. */
|
|
9275
|
+
static createTakePictureButton(texts) {
|
|
9276
|
+
return UploadWidgetUtils.createCaptureButton(texts.takePicture, 'camera');
|
|
9277
|
+
}
|
|
9278
|
+
/** The button that starts a video recording. */
|
|
9279
|
+
static createStartRecordingButton(texts) {
|
|
9280
|
+
return UploadWidgetUtils.createCaptureButton(texts.startRecording, 'fiber_manual_record');
|
|
9281
|
+
}
|
|
9282
|
+
/** The button that stops a recording, in the colour of its host. */
|
|
9283
|
+
static createStopRecordingButton(texts, color) {
|
|
9284
|
+
return UploadWidgetUtils.createCaptureButton(texts.stopRecording, 'stop', color);
|
|
9285
|
+
}
|
|
9286
|
+
/** The button that discards the kept capture for a new one. */
|
|
9287
|
+
static createRetakeButton(texts, type = UiActionButtonType.RAISED) {
|
|
9288
|
+
return UploadWidgetUtils.createCaptureButton(texts.retake, 'replay', 'primary', type);
|
|
9289
|
+
}
|
|
9290
|
+
/** The button that accepts the kept capture. */
|
|
9291
|
+
static createUseButton(texts) {
|
|
9292
|
+
return UploadWidgetUtils.createCaptureButton(texts.use, 'check');
|
|
9293
|
+
}
|
|
9209
9294
|
/** The zone's texts: a descriptor's text per key, the client's default for a missing one. */
|
|
9210
9295
|
static resolveTexts(texts) {
|
|
9211
9296
|
return { ...UPLOAD_ZONE_DEFAULT_TEXTS, ...texts };
|
|
@@ -9456,54 +9541,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
9456
9541
|
type: Input
|
|
9457
9542
|
}], useIconButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "useIconButton", required: false }] }], isMultiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "isMultiple", required: false }] }], autoUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoUpload", required: false }] }], isDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "isDisabled", required: false }] }], pickerShown: [{ type: i0.Input, args: [{ isSignal: true, alias: "pickerShown", required: false }] }] } });
|
|
9458
9543
|
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
this.
|
|
9464
|
-
|
|
9465
|
-
this.
|
|
9466
|
-
...(ngDevMode ? [
|
|
9467
|
-
/**
|
|
9468
|
-
this.mode = input('PHOTO', /* @ts-ignore */
|
|
9469
|
-
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
9470
|
-
/** True when the zone's capture row carries the button: the widget then opens only by
|
|
9471
|
-
* openCapture(). */
|
|
9472
|
-
this.externalTrigger = input(false, /* @ts-ignore */
|
|
9473
|
-
...(ngDevMode ? [{ debugName: "externalTrigger" }] : /* istanbul ignore next */ []));
|
|
9474
|
-
/** The zone's texts; the buttons follow them. */
|
|
9475
|
-
this.texts = input(UPLOAD_ZONE_DEFAULT_TEXTS, /* @ts-ignore */
|
|
9476
|
-
...(ngDevMode ? [{ debugName: "texts" }] : /* istanbul ignore next */ []));
|
|
9477
|
-
/** AUTO asks for the camera when the widget opens, MANUAL at the switch-on button. */
|
|
9478
|
-
this.deviceStart = input(CaptureDeviceStart.MANUAL, /* @ts-ignore */
|
|
9479
|
-
...(ngDevMode ? [{ debugName: "deviceStart" }] : /* istanbul ignore next */ []));
|
|
9480
|
-
/** CONFIRM keeps a capture for retake or use, IMMEDIATE hands it over at the take or the stop. */
|
|
9481
|
-
this.handover = input(CaptureHandover.IMMEDIATE, /* @ts-ignore */
|
|
9482
|
-
...(ngDevMode ? [{ debugName: "handover" }] : /* istanbul ignore next */ []));
|
|
9483
|
-
/** True in a host that draws actionButtonsTemplate: the widget then lays out as the Capture
|
|
9484
|
-
* Dialog's camera view, with the switch inside the view. */
|
|
9485
|
-
this.actionsInHost = input(false, /* @ts-ignore */
|
|
9486
|
-
...(ngDevMode ? [{ debugName: "actionsInHost" }] : /* istanbul ignore next */ []));
|
|
9487
|
-
this.photoCaptured = output();
|
|
9488
|
-
/** The desktop webcam panel of an externally triggered photo widget, opened by openCapture(). */
|
|
9489
|
-
this.isPanelOpen = signal(false, /* @ts-ignore */
|
|
9490
|
-
...(ngDevMode ? [{ debugName: "isPanelOpen" }] : /* istanbul ignore next */ []));
|
|
9491
|
-
/**
|
|
9492
|
-
* Both are written after an `await` — the camera stream, the compression — so a signal
|
|
9493
|
-
* each: the click that started it has long been checked by then.
|
|
9494
|
-
*/
|
|
9495
|
-
this.cameraActiveState = signal(false, /* @ts-ignore */
|
|
9496
|
-
...(ngDevMode ? [{ debugName: "cameraActiveState" }] : /* istanbul ignore next */ []));
|
|
9497
|
-
/** Set when the browser gives no camera, shown in place of the switch-on hint. */
|
|
9544
|
+
/** The camera of one capture view: the stream, the recorder and the capture kept for a decision.
|
|
9545
|
+
* The view calls release() when it closes. */
|
|
9546
|
+
class CameraDevice {
|
|
9547
|
+
constructor(options) {
|
|
9548
|
+
this.options = options;
|
|
9549
|
+
/** A signal, because it is set after an `await`, outside the change detection of the click. */
|
|
9550
|
+
this.isCameraActive = signal(false, /* @ts-ignore */
|
|
9551
|
+
...(ngDevMode ? [{ debugName: "isCameraActive" }] : /* istanbul ignore next */ []));
|
|
9552
|
+
/** Set when the browser gives no camera. */
|
|
9498
9553
|
this.cameraError = signal(false, /* @ts-ignore */
|
|
9499
9554
|
...(ngDevMode ? [{ debugName: "cameraError" }] : /* istanbul ignore next */ []));
|
|
9500
|
-
/** True while a camera request runs
|
|
9555
|
+
/** True while a camera request runs. */
|
|
9501
9556
|
this.isCameraStarting = signal(false, /* @ts-ignore */
|
|
9502
9557
|
...(ngDevMode ? [{ debugName: "isCameraStarting" }] : /* istanbul ignore next */ []));
|
|
9503
|
-
this.
|
|
9504
|
-
...(ngDevMode ? [{ debugName: "
|
|
9505
|
-
this.
|
|
9506
|
-
...(ngDevMode ? [{ debugName: "
|
|
9558
|
+
this.isRecording = signal(false, /* @ts-ignore */
|
|
9559
|
+
...(ngDevMode ? [{ debugName: "isRecording" }] : /* istanbul ignore next */ []));
|
|
9560
|
+
this.isCaptured = signal(false, /* @ts-ignore */
|
|
9561
|
+
...(ngDevMode ? [{ debugName: "isCaptured" }] : /* istanbul ignore next */ []));
|
|
9507
9562
|
/** A capture kept for retake or use under the CONFIRM handover. */
|
|
9508
9563
|
this.keptFiles = signal(undefined, /* @ts-ignore */
|
|
9509
9564
|
...(ngDevMode ? [{ debugName: "keptFiles" }] : /* istanbul ignore next */ []));
|
|
@@ -9513,110 +9568,16 @@ class PhotoCaptureWidgetComponent {
|
|
|
9513
9568
|
/** The milliseconds of the running video, kept as the length of the stopped one. */
|
|
9514
9569
|
this.recordingTime = signal(0, /* @ts-ignore */
|
|
9515
9570
|
...(ngDevMode ? [{ debugName: "recordingTime" }] : /* istanbul ignore next */ []));
|
|
9516
|
-
/** The line on the stage of a running video: a timer in a host, the message on the panel. */
|
|
9517
|
-
this.recordingCaption = computed(() => this.actionsInHost()
|
|
9518
|
-
? UploadWidgetUtils.formatDuration(this.recordingTime())
|
|
9519
|
-
: this.texts().recording, /* @ts-ignore */
|
|
9520
|
-
...(ngDevMode ? [{ debugName: "recordingCaption" }] : /* istanbul ignore next */ []));
|
|
9521
|
-
/** The kept capture's line on the stage of a host; the panel has none. */
|
|
9522
|
-
this.capturedCaption = computed(() => {
|
|
9523
|
-
const files = this.keptFiles();
|
|
9524
|
-
if (!files || !this.actionsInHost()) {
|
|
9525
|
-
return undefined;
|
|
9526
|
-
}
|
|
9527
|
-
const size = UploadWidgetUtils.formatCapturedSize(files[0].size);
|
|
9528
|
-
if (this.mode() === 'PHOTO') {
|
|
9529
|
-
return `${this.texts().capturedPhoto} · ${size}`;
|
|
9530
|
-
}
|
|
9531
|
-
const duration = UploadWidgetUtils.formatDuration(this.recordingTime());
|
|
9532
|
-
return `${this.texts().capturedVideo} · ${duration} · ${size}`;
|
|
9533
|
-
}, /* @ts-ignore */
|
|
9534
|
-
...(ngDevMode ? [{ debugName: "capturedCaption" }] : /* istanbul ignore next */ []));
|
|
9535
9571
|
/** True while a host may not lose the capture by accident: a device is asked for or runs, or
|
|
9536
9572
|
* a capture waits for confirmation. */
|
|
9537
|
-
this.isCaptureUnderWay = computed(() => this.isCameraStarting() ||
|
|
9573
|
+
this.isCaptureUnderWay = computed(() => this.isCameraStarting() ||
|
|
9574
|
+
this.isCameraActive() ||
|
|
9575
|
+
this.isRecording() ||
|
|
9576
|
+
!!this.keptFiles(), /* @ts-ignore */
|
|
9538
9577
|
...(ngDevMode ? [{ debugName: "isCaptureUnderWay" }] : /* istanbul ignore next */ []));
|
|
9539
|
-
/** Under AUTO and in a host the switch returns when the camera is off, so a second shot stays
|
|
9540
|
-
* possible. */
|
|
9541
|
-
this.isCameraSwitchShown = computed(() => !this.keptFiles() &&
|
|
9542
|
-
((this.deviceStart() === CaptureDeviceStart.MANUAL && !this.actionsInHost()) ||
|
|
9543
|
-
!(this.cameraActive || this.isCameraStarting())), /* @ts-ignore */
|
|
9544
|
-
...(ngDevMode ? [{ debugName: "isCameraSwitchShown" }] : /* istanbul ignore next */ []));
|
|
9545
|
-
this.isSwitchOnHintShown = computed(() => this.isCameraSwitchShown() && !this.cameraActive && !this.isCaptured, /* @ts-ignore */
|
|
9546
|
-
...(ngDevMode ? [{ debugName: "isSwitchOnHintShown" }] : /* istanbul ignore next */ []));
|
|
9547
9578
|
this.recordingStartTime = 0;
|
|
9548
9579
|
/** Advanced by stopCamera(), so a stream the browser gives after a close is stopped at once. */
|
|
9549
9580
|
this.cameraRequest = 0;
|
|
9550
|
-
this.toggleCameraButton = computed(() => this.createButton(this.cameraActive ? this.texts().cameraOff : this.texts().cameraOn, 'power_settings_new', this.cameraActive ? 'red' : 'primary', this.getButtonType(UiActionButtonType.NORMAL)), /* @ts-ignore */
|
|
9551
|
-
...(ngDevMode ? [{ debugName: "toggleCameraButton" }] : /* istanbul ignore next */ []));
|
|
9552
|
-
this.captureButton = computed(() => this.createButton(this.texts().takePicture, 'camera'), /* @ts-ignore */
|
|
9553
|
-
...(ngDevMode ? [{ debugName: "captureButton" }] : /* istanbul ignore next */ []));
|
|
9554
|
-
this.startRecordingButton = computed(() => this.createButton(this.texts().startRecording, 'fiber_manual_record'), /* @ts-ignore */
|
|
9555
|
-
...(ngDevMode ? [{ debugName: "startRecordingButton" }] : /* istanbul ignore next */ []));
|
|
9556
|
-
this.stopRecordingButton = computed(() => this.createButton(this.texts().stopRecording, 'stop', this.actionsInHost() ? 'warn' : 'red'), /* @ts-ignore */
|
|
9557
|
-
...(ngDevMode ? [{ debugName: "stopRecordingButton" }] : /* istanbul ignore next */ []));
|
|
9558
|
-
this.pickVideoButton = computed(() => this.createButton(this.texts().pickVideo, 'video_library'), /* @ts-ignore */
|
|
9559
|
-
...(ngDevMode ? [{ debugName: "pickVideoButton" }] : /* istanbul ignore next */ []));
|
|
9560
|
-
this.retakeButton = computed(() => this.createButton(this.texts().retake, 'replay', 'primary', this.getButtonType(UiActionButtonType.NORMAL)), /* @ts-ignore */
|
|
9561
|
-
...(ngDevMode ? [{ debugName: "retakeButton" }] : /* istanbul ignore next */ []));
|
|
9562
|
-
this.useButton = computed(() => this.createButton(this.texts().use, 'check'), /* @ts-ignore */
|
|
9563
|
-
...(ngDevMode ? [{ debugName: "useButton" }] : /* istanbul ignore next */ []));
|
|
9564
|
-
}
|
|
9565
|
-
get cameraActive() {
|
|
9566
|
-
return this.cameraActiveState();
|
|
9567
|
-
}
|
|
9568
|
-
set cameraActive(active) {
|
|
9569
|
-
this.cameraActiveState.set(active);
|
|
9570
|
-
}
|
|
9571
|
-
get isRecording() {
|
|
9572
|
-
return this.isRecordingState();
|
|
9573
|
-
}
|
|
9574
|
-
set isRecording(recording) {
|
|
9575
|
-
this.isRecordingState.set(recording);
|
|
9576
|
-
}
|
|
9577
|
-
get isCaptured() {
|
|
9578
|
-
return this.isCapturedState();
|
|
9579
|
-
}
|
|
9580
|
-
set isCaptured(captured) {
|
|
9581
|
-
this.isCapturedState.set(captured);
|
|
9582
|
-
}
|
|
9583
|
-
isMobile() {
|
|
9584
|
-
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
9585
|
-
}
|
|
9586
|
-
/** A phone opens its own camera, a desktop the webcam panel. */
|
|
9587
|
-
openCapture() {
|
|
9588
|
-
if (this.isMobile()) {
|
|
9589
|
-
this.fileInputRef.nativeElement.click();
|
|
9590
|
-
return;
|
|
9591
|
-
}
|
|
9592
|
-
if (this.isPanelOpen()) {
|
|
9593
|
-
this.stopCamera();
|
|
9594
|
-
this.discardKeptCapture();
|
|
9595
|
-
}
|
|
9596
|
-
else {
|
|
9597
|
-
this.startCameraAfterRender();
|
|
9598
|
-
}
|
|
9599
|
-
this.isPanelOpen.update((open) => !open);
|
|
9600
|
-
}
|
|
9601
|
-
ngAfterViewInit() {
|
|
9602
|
-
if (!this.externalTrigger()) {
|
|
9603
|
-
this.startCameraAfterRender();
|
|
9604
|
-
}
|
|
9605
|
-
}
|
|
9606
|
-
ngOnDestroy() {
|
|
9607
|
-
this.stopCamera();
|
|
9608
|
-
this.discardKeptCapture();
|
|
9609
|
-
}
|
|
9610
|
-
/** AUTO waits for the render: the stream goes into the panel's video element. */
|
|
9611
|
-
startCameraAfterRender() {
|
|
9612
|
-
if (this.deviceStart() !== CaptureDeviceStart.AUTO || this.isMobile()) {
|
|
9613
|
-
return;
|
|
9614
|
-
}
|
|
9615
|
-
afterNextRender(() => {
|
|
9616
|
-
if (!this.externalTrigger() || this.isPanelOpen()) {
|
|
9617
|
-
this.startCamera();
|
|
9618
|
-
}
|
|
9619
|
-
}, { injector: this.injector });
|
|
9620
9581
|
}
|
|
9621
9582
|
async startCamera() {
|
|
9622
9583
|
this.cameraError.set(false);
|
|
@@ -9638,15 +9599,16 @@ class PhotoCaptureWidgetComponent {
|
|
|
9638
9599
|
return;
|
|
9639
9600
|
}
|
|
9640
9601
|
this.stream = stream;
|
|
9641
|
-
|
|
9642
|
-
this.
|
|
9602
|
+
const video = this.options.getVideoElement();
|
|
9603
|
+
video.srcObject = this.stream;
|
|
9604
|
+
video.play();
|
|
9643
9605
|
this.isCameraStarting.set(false);
|
|
9644
|
-
this.
|
|
9645
|
-
this.isCaptured
|
|
9606
|
+
this.isCameraActive.set(true);
|
|
9607
|
+
this.isCaptured.set(false);
|
|
9646
9608
|
}
|
|
9647
9609
|
/** A video asks for the sound too; a desktop without a microphone records a silent one. */
|
|
9648
9610
|
async openCameraStream() {
|
|
9649
|
-
if (this.mode() === 'VIDEO') {
|
|
9611
|
+
if (this.options.mode() === 'VIDEO') {
|
|
9650
9612
|
try {
|
|
9651
9613
|
return await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
|
|
9652
9614
|
}
|
|
@@ -9666,14 +9628,22 @@ class PhotoCaptureWidgetComponent {
|
|
|
9666
9628
|
this.recorder.stop();
|
|
9667
9629
|
this.recorder = undefined;
|
|
9668
9630
|
}
|
|
9669
|
-
this.isRecording
|
|
9631
|
+
this.isRecording.set(false);
|
|
9670
9632
|
this.stream?.getTracks().forEach((track) => track.stop());
|
|
9671
9633
|
this.stream = undefined;
|
|
9672
|
-
this.
|
|
9634
|
+
this.isCameraActive.set(false);
|
|
9635
|
+
}
|
|
9636
|
+
toggleCamera() {
|
|
9637
|
+
if (this.isCameraActive()) {
|
|
9638
|
+
this.stopCamera();
|
|
9639
|
+
}
|
|
9640
|
+
else {
|
|
9641
|
+
this.startCamera();
|
|
9642
|
+
}
|
|
9673
9643
|
}
|
|
9674
9644
|
async capturePhoto() {
|
|
9675
|
-
const video = this.
|
|
9676
|
-
const canvas = this.
|
|
9645
|
+
const video = this.options.getVideoElement();
|
|
9646
|
+
const canvas = this.options.getCanvasElement();
|
|
9677
9647
|
const context = canvas.getContext('2d');
|
|
9678
9648
|
if (!context)
|
|
9679
9649
|
return;
|
|
@@ -9684,12 +9654,12 @@ class PhotoCaptureWidgetComponent {
|
|
|
9684
9654
|
if (!blob)
|
|
9685
9655
|
return;
|
|
9686
9656
|
let file = new File([blob], `photo_${Date.now()}.jpg`, { type: 'image/jpeg' });
|
|
9687
|
-
const maxFileSize = this.maxFileSize();
|
|
9657
|
+
const maxFileSize = this.options.maxFileSize();
|
|
9688
9658
|
if (maxFileSize && file.size > maxFileSize) {
|
|
9689
|
-
file = await
|
|
9659
|
+
file = await UploadWidgetUtils.compressImageFile(file, maxFileSize);
|
|
9690
9660
|
}
|
|
9691
9661
|
this.handOverCapture([file]);
|
|
9692
|
-
this.isCaptured
|
|
9662
|
+
this.isCaptured.set(true);
|
|
9693
9663
|
this.stopCamera();
|
|
9694
9664
|
}
|
|
9695
9665
|
/** A browser that records neither webm nor mp4 refuses the recorder: the camera error shows. */
|
|
@@ -9715,7 +9685,7 @@ class PhotoCaptureWidgetComponent {
|
|
|
9715
9685
|
};
|
|
9716
9686
|
recorder.start();
|
|
9717
9687
|
this.recorder = recorder;
|
|
9718
|
-
this.isRecording
|
|
9688
|
+
this.isRecording.set(true);
|
|
9719
9689
|
this.recordingStartTime = Date.now();
|
|
9720
9690
|
this.recordingTime.set(0);
|
|
9721
9691
|
this.recordingClock = setInterval(() => this.updateRecordingTime(), 1000);
|
|
@@ -9728,23 +9698,28 @@ class PhotoCaptureWidgetComponent {
|
|
|
9728
9698
|
this.recorder?.stop();
|
|
9729
9699
|
}
|
|
9730
9700
|
handOverCapture(files) {
|
|
9731
|
-
if (this.handover() === CaptureHandover.IMMEDIATE) {
|
|
9732
|
-
this.
|
|
9701
|
+
if (this.options.handover() === CaptureHandover.IMMEDIATE) {
|
|
9702
|
+
this.options.handOver(files);
|
|
9733
9703
|
return;
|
|
9734
9704
|
}
|
|
9735
9705
|
this.keptFiles.set(files);
|
|
9736
|
-
if (this.mode() === 'VIDEO') {
|
|
9706
|
+
if (this.options.mode() === 'VIDEO') {
|
|
9737
9707
|
this.keptVideoUrl.set(URL.createObjectURL(files[0]));
|
|
9738
9708
|
}
|
|
9739
9709
|
}
|
|
9740
9710
|
retakeCapture() {
|
|
9741
9711
|
this.discardKeptCapture();
|
|
9742
|
-
if (this.deviceStart() === CaptureDeviceStart.AUTO) {
|
|
9712
|
+
if (this.options.deviceStart() === CaptureDeviceStart.AUTO) {
|
|
9743
9713
|
this.startCamera();
|
|
9744
9714
|
}
|
|
9745
9715
|
}
|
|
9746
9716
|
useCapture() {
|
|
9747
|
-
this.
|
|
9717
|
+
this.options.handOver(this.keptFiles());
|
|
9718
|
+
this.discardKeptCapture();
|
|
9719
|
+
}
|
|
9720
|
+
/** Stops the camera and drops what it kept: the close of the view. */
|
|
9721
|
+
release() {
|
|
9722
|
+
this.stopCamera();
|
|
9748
9723
|
this.discardKeptCapture();
|
|
9749
9724
|
}
|
|
9750
9725
|
discardKeptCapture() {
|
|
@@ -9754,250 +9729,141 @@ class PhotoCaptureWidgetComponent {
|
|
|
9754
9729
|
}
|
|
9755
9730
|
this.keptVideoUrl.set(undefined);
|
|
9756
9731
|
this.keptFiles.set(undefined);
|
|
9757
|
-
this.isCaptured
|
|
9758
|
-
}
|
|
9759
|
-
getPhotoDataURL() {
|
|
9760
|
-
return this.canvasRef.nativeElement.toDataURL('image/png');
|
|
9761
|
-
}
|
|
9762
|
-
async onFileSelected(event) {
|
|
9763
|
-
const input = event.target;
|
|
9764
|
-
if (!input.files || input.files.length === 0)
|
|
9765
|
-
return;
|
|
9766
|
-
const maxSize = this.maxFileSize();
|
|
9767
|
-
const filesArray = Array.from(input.files);
|
|
9768
|
-
const processedFiles = [];
|
|
9769
|
-
for (const file of filesArray) {
|
|
9770
|
-
if (maxSize && file.size > maxSize && file.type.startsWith('image/')) {
|
|
9771
|
-
try {
|
|
9772
|
-
const compressed = await this.compressImageFile(file, maxSize);
|
|
9773
|
-
processedFiles.push(compressed);
|
|
9774
|
-
}
|
|
9775
|
-
catch {
|
|
9776
|
-
processedFiles.push(file);
|
|
9777
|
-
}
|
|
9778
|
-
}
|
|
9779
|
-
else {
|
|
9780
|
-
processedFiles.push(file);
|
|
9781
|
-
}
|
|
9782
|
-
}
|
|
9783
|
-
this.photoCaptured.emit(processedFiles);
|
|
9784
|
-
}
|
|
9785
|
-
toggleCamera() {
|
|
9786
|
-
if (this.cameraActive) {
|
|
9787
|
-
this.stopCamera();
|
|
9788
|
-
}
|
|
9789
|
-
else {
|
|
9790
|
-
this.startCamera();
|
|
9791
|
-
}
|
|
9792
|
-
}
|
|
9793
|
-
async compressImageFile(file, maxFileSize) {
|
|
9794
|
-
if (!file.type.startsWith('image/'))
|
|
9795
|
-
return file;
|
|
9796
|
-
const img = await new Promise((resolve, reject) => {
|
|
9797
|
-
const url = URL.createObjectURL(file);
|
|
9798
|
-
const image = new Image();
|
|
9799
|
-
image.onload = () => {
|
|
9800
|
-
URL.revokeObjectURL(url);
|
|
9801
|
-
resolve(image);
|
|
9802
|
-
};
|
|
9803
|
-
image.onerror = (e) => {
|
|
9804
|
-
URL.revokeObjectURL(url);
|
|
9805
|
-
reject(e);
|
|
9806
|
-
};
|
|
9807
|
-
image.src = url;
|
|
9808
|
-
});
|
|
9809
|
-
const canvas = document.createElement('canvas');
|
|
9810
|
-
const ctx = canvas.getContext('2d');
|
|
9811
|
-
if (!ctx)
|
|
9812
|
-
return file;
|
|
9813
|
-
let width = img.naturalWidth;
|
|
9814
|
-
let height = img.naturalHeight;
|
|
9815
|
-
let quality = 0.9;
|
|
9816
|
-
const minQuality = 0.4;
|
|
9817
|
-
const minDimension = 640;
|
|
9818
|
-
const getBlob = (q) => new Promise((resolve) => {
|
|
9819
|
-
canvas.width = width;
|
|
9820
|
-
canvas.height = height;
|
|
9821
|
-
ctx.clearRect(0, 0, width, height);
|
|
9822
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
9823
|
-
canvas.toBlob((blob) => {
|
|
9824
|
-
if (blob)
|
|
9825
|
-
resolve(blob);
|
|
9826
|
-
}, 'image/jpeg', q);
|
|
9827
|
-
});
|
|
9828
|
-
let blob = await getBlob(quality);
|
|
9829
|
-
while (blob.size > maxFileSize) {
|
|
9830
|
-
if (quality > minQuality) {
|
|
9831
|
-
quality -= 0.1;
|
|
9832
|
-
}
|
|
9833
|
-
else if (width > minDimension && height > minDimension) {
|
|
9834
|
-
width = Math.floor(width * 0.8);
|
|
9835
|
-
height = Math.floor(height * 0.8);
|
|
9836
|
-
quality = 0.9;
|
|
9837
|
-
}
|
|
9838
|
-
else {
|
|
9839
|
-
break;
|
|
9840
|
-
}
|
|
9841
|
-
blob = await getBlob(quality);
|
|
9842
|
-
}
|
|
9843
|
-
if (blob.size <= maxFileSize) {
|
|
9844
|
-
return new File([blob], file.name, { type: 'image/jpeg' });
|
|
9845
|
-
}
|
|
9846
|
-
else {
|
|
9847
|
-
return file;
|
|
9848
|
-
}
|
|
9849
|
-
}
|
|
9850
|
-
/** The inline bar's buttons are all raised; a host's view tells them apart by type. */
|
|
9851
|
-
getButtonType(typeInHost) {
|
|
9852
|
-
return this.actionsInHost() ? typeInHost : UiActionButtonType.RAISED;
|
|
9732
|
+
this.isCaptured.set(false);
|
|
9853
9733
|
}
|
|
9854
|
-
createButton(title, icon, color = 'primary', type = UiActionButtonType.RAISED) {
|
|
9855
|
-
return { type, title, icon, iconPosition: IconPosition.PRE, color };
|
|
9856
|
-
}
|
|
9857
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PhotoCaptureWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
9858
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: PhotoCaptureWidgetComponent, isStandalone: true, selector: "photo-capture-widget", inputs: { maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, externalTrigger: { classPropertyName: "externalTrigger", publicName: "externalTrigger", isSignal: true, isRequired: false, transformFunction: null }, texts: { classPropertyName: "texts", publicName: "texts", isSignal: true, isRequired: false, transformFunction: null }, deviceStart: { classPropertyName: "deviceStart", publicName: "deviceStart", isSignal: true, isRequired: false, transformFunction: null }, handover: { classPropertyName: "handover", publicName: "handover", isSignal: true, isRequired: false, transformFunction: null }, actionsInHost: { classPropertyName: "actionsInHost", publicName: "actionsInHost", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { photoCaptured: "photoCaptured" }, viewQueries: [{ propertyName: "actionButtonsTemplate", first: true, predicate: ["actionButtons"], descendants: true, isSignal: true }, { propertyName: "videoRef", first: true, predicate: ["video"], descendants: true }, { propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true }, { propertyName: "fileInputRef", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: "@if (isMobile()) {\n <input\n #fileInput\n type=\"file\"\n [accept]=\"mode() === 'VIDEO' ? 'video/*' : 'image/*'\"\n capture=\"environment\"\n (change)=\"onFileSelected($event)\"\n style=\"display: none\"\n />\n @if (!externalTrigger()) {\n <div class=\"photoWidgetContainer\">\n <div class=\"controls\">\n <ui-action-button [descriptor]=\"captureButton()\" (actionClick)=\"fileInput.click()\">\n </ui-action-button>\n </div>\n </div>\n }\n} @else if (!externalTrigger() || isPanelOpen()) {\n <div class=\"photoWidgetContainer\" [class.actionsInHost]=\"actionsInHost()\">\n <div class=\"cameraStage\">\n <video #video autoplay [muted]=\"true\" [hidden]=\"isCaptured || !cameraActive\"></video>\n <canvas #canvas [hidden]=\"!isCaptured || cameraActive\"></canvas>\n @if (keptVideoUrl(); as videoUrl) {\n <video controls [src]=\"videoUrl\"></video>\n }\n @if (cameraError()) {\n <p class=\"message cameraErrorMessage\">{{ texts().cameraError }}</p>\n } @else if (capturedCaption(); as caption) {\n <p class=\"message capturedCaption\">{{ caption }}</p>\n } @else if (isRecording) {\n <p class=\"message recordingMessage\">{{ recordingCaption() }}</p>\n } @else if (isSwitchOnHintShown()) {\n @if (mode() === 'VIDEO') {\n <p class=\"message\">{{ texts().switchOnForVideo }}</p>\n } @else {\n <p class=\"message\">{{ texts().switchOnCameraHint }}</p>\n }\n }\n @if (actionsInHost()) {\n <ng-container [ngTemplateOutlet]=\"cameraSwitch\" />\n }\n </div>\n @if (!actionsInHost()) {\n <div class=\"controls\">\n <ng-container [ngTemplateOutlet]=\"cameraSwitch\" />\n <ng-container [ngTemplateOutlet]=\"actionButtons\" />\n @if (mode() === 'VIDEO' && !keptFiles()) {\n <!-- PICK VIDEO FILE -->\n <input\n #fileInput\n type=\"file\"\n accept=\"video/*\"\n (change)=\"onFileSelected($event)\"\n style=\"display: none\"\n />\n <ui-action-button\n class=\"pickVideoButton\"\n [disabled]=\"isRecording\"\n [descriptor]=\"pickVideoButton()\"\n (actionClick)=\"fileInput.click()\"\n >\n </ui-action-button>\n }\n </div>\n }\n </div>\n}\n\n<ng-template #cameraSwitch>\n @if (isCameraSwitchShown()) {\n <!-- START CAMERA -->\n <ui-action-button\n class=\"toggleCameraButton\"\n [disabled]=\"isRecording\"\n [descriptor]=\"toggleCameraButton()\"\n (actionClick)=\"toggleCamera()\"\n >\n </ui-action-button>\n }\n</ng-template>\n\n<ng-template #actionButtons>\n @if (keptFiles()) {\n <ui-action-button\n class=\"retakeButton\"\n [descriptor]=\"retakeButton()\"\n (actionClick)=\"retakeCapture()\"\n >\n </ui-action-button>\n <ui-action-button\n class=\"useButton\"\n [descriptor]=\"useButton()\"\n (actionClick)=\"useCapture()\"\n >\n </ui-action-button>\n } @else if (mode() === 'VIDEO') {\n <!-- RECORD / STOP VIDEO -->\n @if (isRecording) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"stopRecording()\"\n >\n </ui-action-button>\n } @else {\n <ui-action-button\n class=\"startRecordingButton\"\n [disabled]=\"!cameraActive\"\n [descriptor]=\"startRecordingButton()\"\n (actionClick)=\"startRecording()\"\n >\n </ui-action-button>\n }\n } @else {\n <!-- CAPTURE CAMERA -->\n <ui-action-button\n class=\"capturePhotoButton\"\n [disabled]=\"!cameraActive\"\n [descriptor]=\"captureButton()\"\n (actionClick)=\"capturePhoto()\"\n >\n </ui-action-button>\n }\n</ng-template>\n", styles: [".photoWidgetContainer{border-radius:5px;width:100%;max-width:100%;display:flex;flex-direction:column;justify-content:center;border:1px solid #dee2e6}.message{text-align:center;color:#6b7280}video,canvas{border-top-right-radius:5px;border-top-left-radius:5px;width:100%}.controls{padding:.5rem;display:flex;gap:1rem;flex-direction:row;justify-content:center;background:#f8f9fa;border-top:1px solid #dee2e6;border-bottom-right-radius:5px;border-bottom-left-radius:5px}.controls ::ng-deep button{border-radius:3px}.cameraStage{display:contents}.actionsInHost{border:unset}.actionsInHost .cameraStage{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px;--stage-background: var( --capture-stage-background, color-mix(in srgb, var(--primary-color) 30%, black) );background:var(--stage-background);color:var(--capture-stage-text, color-mix(in srgb, var(--primary-color) 10%, white))}.actionsInHost .cameraStage video,.actionsInHost .cameraStage canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;border-radius:unset}.actionsInHost .toggleCameraButton ::ng-deep button{background:var(--color-surface)}.actionsInHost .message{margin:0;color:inherit}.actionsInHost .cameraErrorMessage{position:relative;color:color-mix(in srgb,var(--warn-color) 65%,white)}.actionsInHost .recordingMessage,.actionsInHost .capturedCaption{position:absolute;left:.5rem;top:.5rem;padding:.125rem .5rem;border-radius:.5rem;font-size:.75rem;font-weight:500}.actionsInHost .recordingMessage{background:color-mix(in srgb,var(--stage-background) 67%,transparent);font-family:monospace}.actionsInHost .capturedCaption{background:color-mix(in srgb,var(--success-color) 50%,black)}.actionsInHost .recordingMessage:before{content:\"\";display:inline-block;width:.5rem;height:.5rem;margin-right:.3125rem;border-radius:50%;background:var(--warn-color)}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UiActionButtonComponent, selector: "ui-action-button", inputs: ["disabled", "descriptor", "code", "identifier", "viewActivated", "tab", "rowState", "addedCssClass", "addBasicClasses", "iconPlaceholder", "chords"], outputs: ["actionClick", "actionDoubleClick"] }] }); }
|
|
9859
9734
|
}
|
|
9860
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PhotoCaptureWidgetComponent, decorators: [{
|
|
9861
|
-
type: Component,
|
|
9862
|
-
args: [{ selector: 'photo-capture-widget', imports: [NgTemplateOutlet, UiActionButtonComponent], template: "@if (isMobile()) {\n <input\n #fileInput\n type=\"file\"\n [accept]=\"mode() === 'VIDEO' ? 'video/*' : 'image/*'\"\n capture=\"environment\"\n (change)=\"onFileSelected($event)\"\n style=\"display: none\"\n />\n @if (!externalTrigger()) {\n <div class=\"photoWidgetContainer\">\n <div class=\"controls\">\n <ui-action-button [descriptor]=\"captureButton()\" (actionClick)=\"fileInput.click()\">\n </ui-action-button>\n </div>\n </div>\n }\n} @else if (!externalTrigger() || isPanelOpen()) {\n <div class=\"photoWidgetContainer\" [class.actionsInHost]=\"actionsInHost()\">\n <div class=\"cameraStage\">\n <video #video autoplay [muted]=\"true\" [hidden]=\"isCaptured || !cameraActive\"></video>\n <canvas #canvas [hidden]=\"!isCaptured || cameraActive\"></canvas>\n @if (keptVideoUrl(); as videoUrl) {\n <video controls [src]=\"videoUrl\"></video>\n }\n @if (cameraError()) {\n <p class=\"message cameraErrorMessage\">{{ texts().cameraError }}</p>\n } @else if (capturedCaption(); as caption) {\n <p class=\"message capturedCaption\">{{ caption }}</p>\n } @else if (isRecording) {\n <p class=\"message recordingMessage\">{{ recordingCaption() }}</p>\n } @else if (isSwitchOnHintShown()) {\n @if (mode() === 'VIDEO') {\n <p class=\"message\">{{ texts().switchOnForVideo }}</p>\n } @else {\n <p class=\"message\">{{ texts().switchOnCameraHint }}</p>\n }\n }\n @if (actionsInHost()) {\n <ng-container [ngTemplateOutlet]=\"cameraSwitch\" />\n }\n </div>\n @if (!actionsInHost()) {\n <div class=\"controls\">\n <ng-container [ngTemplateOutlet]=\"cameraSwitch\" />\n <ng-container [ngTemplateOutlet]=\"actionButtons\" />\n @if (mode() === 'VIDEO' && !keptFiles()) {\n <!-- PICK VIDEO FILE -->\n <input\n #fileInput\n type=\"file\"\n accept=\"video/*\"\n (change)=\"onFileSelected($event)\"\n style=\"display: none\"\n />\n <ui-action-button\n class=\"pickVideoButton\"\n [disabled]=\"isRecording\"\n [descriptor]=\"pickVideoButton()\"\n (actionClick)=\"fileInput.click()\"\n >\n </ui-action-button>\n }\n </div>\n }\n </div>\n}\n\n<ng-template #cameraSwitch>\n @if (isCameraSwitchShown()) {\n <!-- START CAMERA -->\n <ui-action-button\n class=\"toggleCameraButton\"\n [disabled]=\"isRecording\"\n [descriptor]=\"toggleCameraButton()\"\n (actionClick)=\"toggleCamera()\"\n >\n </ui-action-button>\n }\n</ng-template>\n\n<ng-template #actionButtons>\n @if (keptFiles()) {\n <ui-action-button\n class=\"retakeButton\"\n [descriptor]=\"retakeButton()\"\n (actionClick)=\"retakeCapture()\"\n >\n </ui-action-button>\n <ui-action-button\n class=\"useButton\"\n [descriptor]=\"useButton()\"\n (actionClick)=\"useCapture()\"\n >\n </ui-action-button>\n } @else if (mode() === 'VIDEO') {\n <!-- RECORD / STOP VIDEO -->\n @if (isRecording) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"stopRecording()\"\n >\n </ui-action-button>\n } @else {\n <ui-action-button\n class=\"startRecordingButton\"\n [disabled]=\"!cameraActive\"\n [descriptor]=\"startRecordingButton()\"\n (actionClick)=\"startRecording()\"\n >\n </ui-action-button>\n }\n } @else {\n <!-- CAPTURE CAMERA -->\n <ui-action-button\n class=\"capturePhotoButton\"\n [disabled]=\"!cameraActive\"\n [descriptor]=\"captureButton()\"\n (actionClick)=\"capturePhoto()\"\n >\n </ui-action-button>\n }\n</ng-template>\n", styles: [".photoWidgetContainer{border-radius:5px;width:100%;max-width:100%;display:flex;flex-direction:column;justify-content:center;border:1px solid #dee2e6}.message{text-align:center;color:#6b7280}video,canvas{border-top-right-radius:5px;border-top-left-radius:5px;width:100%}.controls{padding:.5rem;display:flex;gap:1rem;flex-direction:row;justify-content:center;background:#f8f9fa;border-top:1px solid #dee2e6;border-bottom-right-radius:5px;border-bottom-left-radius:5px}.controls ::ng-deep button{border-radius:3px}.cameraStage{display:contents}.actionsInHost{border:unset}.actionsInHost .cameraStage{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px;--stage-background: var( --capture-stage-background, color-mix(in srgb, var(--primary-color) 30%, black) );background:var(--stage-background);color:var(--capture-stage-text, color-mix(in srgb, var(--primary-color) 10%, white))}.actionsInHost .cameraStage video,.actionsInHost .cameraStage canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;border-radius:unset}.actionsInHost .toggleCameraButton ::ng-deep button{background:var(--color-surface)}.actionsInHost .message{margin:0;color:inherit}.actionsInHost .cameraErrorMessage{position:relative;color:color-mix(in srgb,var(--warn-color) 65%,white)}.actionsInHost .recordingMessage,.actionsInHost .capturedCaption{position:absolute;left:.5rem;top:.5rem;padding:.125rem .5rem;border-radius:.5rem;font-size:.75rem;font-weight:500}.actionsInHost .recordingMessage{background:color-mix(in srgb,var(--stage-background) 67%,transparent);font-family:monospace}.actionsInHost .capturedCaption{background:color-mix(in srgb,var(--success-color) 50%,black)}.actionsInHost .recordingMessage:before{content:\"\";display:inline-block;width:.5rem;height:.5rem;margin-right:.3125rem;border-radius:50%;background:var(--warn-color)}\n"] }]
|
|
9863
|
-
}], propDecorators: { videoRef: [{
|
|
9864
|
-
type: ViewChild,
|
|
9865
|
-
args: ['video']
|
|
9866
|
-
}], canvasRef: [{
|
|
9867
|
-
type: ViewChild,
|
|
9868
|
-
args: ['canvas']
|
|
9869
|
-
}], fileInputRef: [{
|
|
9870
|
-
type: ViewChild,
|
|
9871
|
-
args: ['fileInput']
|
|
9872
|
-
}], actionButtonsTemplate: [{ type: i0.ViewChild, args: ['actionButtons', { isSignal: true }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], externalTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "externalTrigger", required: false }] }], texts: [{ type: i0.Input, args: [{ isSignal: true, alias: "texts", required: false }] }], deviceStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "deviceStart", required: false }] }], handover: [{ type: i0.Input, args: [{ isSignal: true, alias: "handover", required: false }] }], actionsInHost: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionsInHost", required: false }] }], photoCaptured: [{ type: i0.Output, args: ["photoCaptured"] }] } });
|
|
9873
9735
|
|
|
9874
|
-
|
|
9736
|
+
/** The camera view of the Capture Dialog: a fixed stage with the switch and the captions inside
|
|
9737
|
+
* it, and the buttons in actionButtonsTemplate for the dialog's footer. */
|
|
9738
|
+
class CaptureCameraViewComponent {
|
|
9875
9739
|
constructor() {
|
|
9876
|
-
this.
|
|
9877
|
-
this.
|
|
9878
|
-
|
|
9740
|
+
this.injector = inject(Injector);
|
|
9741
|
+
this.session = inject(SmartSessionService);
|
|
9742
|
+
this.video = viewChild.required('video', /* @ts-ignore */
|
|
9743
|
+
...(ngDevMode ? [{ debugName: "video" }] : /* istanbul ignore next */ []));
|
|
9744
|
+
this.canvas = viewChild.required('canvas', /* @ts-ignore */
|
|
9745
|
+
...(ngDevMode ? [{ debugName: "canvas" }] : /* istanbul ignore next */ []));
|
|
9879
9746
|
this.actionButtonsTemplate = viewChild('actionButtons', /* @ts-ignore */
|
|
9880
9747
|
...(ngDevMode ? [{ debugName: "actionButtonsTemplate" }] : /* istanbul ignore next */ []));
|
|
9881
|
-
this.
|
|
9882
|
-
...(ngDevMode ? [{ debugName: "
|
|
9883
|
-
|
|
9884
|
-
this.texts = input(UPLOAD_ZONE_DEFAULT_TEXTS, /* @ts-ignore */
|
|
9885
|
-
...(ngDevMode ? [{ debugName: "texts" }] : /* istanbul ignore next */ []));
|
|
9886
|
-
/** True in the upload zone only; the deprecated recordingUploader shows no error line. */
|
|
9887
|
-
this.showMicrophoneError = input(false, /* @ts-ignore */
|
|
9888
|
-
...(ngDevMode ? [{ debugName: "showMicrophoneError" }] : /* istanbul ignore next */ []));
|
|
9889
|
-
/** AUTO starts the recording when the widget opens, MANUAL at the microphone button. */
|
|
9890
|
-
this.deviceStart = input(CaptureDeviceStart.MANUAL, /* @ts-ignore */
|
|
9748
|
+
this.mode = input.required(/* @ts-ignore */
|
|
9749
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
9750
|
+
this.deviceStart = input.required(/* @ts-ignore */
|
|
9891
9751
|
...(ngDevMode ? [{ debugName: "deviceStart" }] : /* istanbul ignore next */ []));
|
|
9892
|
-
|
|
9893
|
-
this.handover = input(CaptureHandover.CONFIRM, /* @ts-ignore */
|
|
9752
|
+
this.handover = input.required(/* @ts-ignore */
|
|
9894
9753
|
...(ngDevMode ? [{ debugName: "handover" }] : /* istanbul ignore next */ []));
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
this.
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
this.
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
};
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
};
|
|
9942
|
-
this.
|
|
9943
|
-
|
|
9944
|
-
: this.saveButton, /* @ts-ignore */
|
|
9945
|
-
...(ngDevMode ? [{ debugName: "effectiveSaveButton" }] : /* istanbul ignore next */ []));
|
|
9946
|
-
this.stopRecordingButton = computed(() => ({
|
|
9947
|
-
title: this.texts().stopRecording,
|
|
9948
|
-
type: UiActionButtonType.RAISED,
|
|
9949
|
-
icon: 'stop',
|
|
9950
|
-
iconPosition: IconPosition.PRE,
|
|
9951
|
-
color: 'warn',
|
|
9952
|
-
}), /* @ts-ignore */
|
|
9953
|
-
...(ngDevMode ? [{ debugName: "stopRecordingButton" }] : /* istanbul ignore next */ []));
|
|
9954
|
-
}
|
|
9955
|
-
ngOnInit() {
|
|
9956
|
-
this.playButton = {
|
|
9957
|
-
title: '',
|
|
9958
|
-
type: UiActionButtonType.ICON,
|
|
9959
|
-
icon: 'play_circle',
|
|
9960
|
-
iconPosition: IconPosition.PRE,
|
|
9961
|
-
color: 'primary',
|
|
9962
|
-
};
|
|
9963
|
-
this.stopPlaybackButton = {
|
|
9964
|
-
title: '',
|
|
9965
|
-
type: UiActionButtonType.ICON,
|
|
9966
|
-
icon: 'stop_circle',
|
|
9967
|
-
iconPosition: IconPosition.PRE,
|
|
9968
|
-
color: this.actionsInHost() ? 'warn' : 'red',
|
|
9969
|
-
};
|
|
9754
|
+
this.texts = input.required(/* @ts-ignore */
|
|
9755
|
+
...(ngDevMode ? [{ debugName: "texts" }] : /* istanbul ignore next */ []));
|
|
9756
|
+
this.maxFileSize = input(/* @ts-ignore */
|
|
9757
|
+
...(ngDevMode ? [undefined, { debugName: "maxFileSize" }] : /* istanbul ignore next */ []));
|
|
9758
|
+
this.captured = output();
|
|
9759
|
+
this.device = new CameraDevice({
|
|
9760
|
+
mode: this.mode,
|
|
9761
|
+
deviceStart: this.deviceStart,
|
|
9762
|
+
handover: this.handover,
|
|
9763
|
+
maxFileSize: this.maxFileSize,
|
|
9764
|
+
getVideoElement: () => this.video().nativeElement,
|
|
9765
|
+
getCanvasElement: () => this.canvas().nativeElement,
|
|
9766
|
+
handOver: (files) => this.captured.emit(files),
|
|
9767
|
+
});
|
|
9768
|
+
/** The timer on the stage of a running video. */
|
|
9769
|
+
this.recordingCaption = computed(() => UploadWidgetUtils.formatDuration(this.device.recordingTime()), /* @ts-ignore */
|
|
9770
|
+
...(ngDevMode ? [{ debugName: "recordingCaption" }] : /* istanbul ignore next */ []));
|
|
9771
|
+
/** The kept capture's line on the stage. */
|
|
9772
|
+
this.capturedCaption = computed(() => {
|
|
9773
|
+
const files = this.device.keptFiles();
|
|
9774
|
+
if (!files) {
|
|
9775
|
+
return undefined;
|
|
9776
|
+
}
|
|
9777
|
+
const size = UploadWidgetUtils.formatCapturedSize(files[0].size, this.session.sessionInfoData?.locale);
|
|
9778
|
+
if (this.mode() === 'PHOTO') {
|
|
9779
|
+
return `${this.texts().capturedPhoto} · ${size}`;
|
|
9780
|
+
}
|
|
9781
|
+
const duration = UploadWidgetUtils.formatDuration(this.device.recordingTime());
|
|
9782
|
+
return `${this.texts().capturedVideo} · ${duration} · ${size}`;
|
|
9783
|
+
}, /* @ts-ignore */
|
|
9784
|
+
...(ngDevMode ? [{ debugName: "capturedCaption" }] : /* istanbul ignore next */ []));
|
|
9785
|
+
/** The switch shows while the camera is off, so a second shot stays possible. */
|
|
9786
|
+
this.isCameraSwitchShown = computed(() => !this.device.keptFiles() &&
|
|
9787
|
+
!(this.device.isCameraActive() || this.device.isCameraStarting()), /* @ts-ignore */
|
|
9788
|
+
...(ngDevMode ? [{ debugName: "isCameraSwitchShown" }] : /* istanbul ignore next */ []));
|
|
9789
|
+
this.isSwitchOnHintShown = computed(() => this.isCameraSwitchShown() && !this.device.isCaptured(), /* @ts-ignore */
|
|
9790
|
+
...(ngDevMode ? [{ debugName: "isSwitchOnHintShown" }] : /* istanbul ignore next */ []));
|
|
9791
|
+
this.switchOnButton = computed(() => UploadWidgetUtils.createCaptureButton(this.texts().cameraOn, 'power_settings_new', 'primary', UiActionButtonType.NORMAL), /* @ts-ignore */
|
|
9792
|
+
...(ngDevMode ? [{ debugName: "switchOnButton" }] : /* istanbul ignore next */ []));
|
|
9793
|
+
this.captureButton = computed(() => UploadWidgetUtils.createTakePictureButton(this.texts()), /* @ts-ignore */
|
|
9794
|
+
...(ngDevMode ? [{ debugName: "captureButton" }] : /* istanbul ignore next */ []));
|
|
9795
|
+
this.startRecordingButton = computed(() => UploadWidgetUtils.createStartRecordingButton(this.texts()), /* @ts-ignore */
|
|
9796
|
+
...(ngDevMode ? [{ debugName: "startRecordingButton" }] : /* istanbul ignore next */ []));
|
|
9797
|
+
this.stopRecordingButton = computed(() => UploadWidgetUtils.createStopRecordingButton(this.texts(), 'warn'), /* @ts-ignore */
|
|
9798
|
+
...(ngDevMode ? [{ debugName: "stopRecordingButton" }] : /* istanbul ignore next */ []));
|
|
9799
|
+
this.retakeButton = computed(() => UploadWidgetUtils.createRetakeButton(this.texts(), UiActionButtonType.NORMAL), /* @ts-ignore */
|
|
9800
|
+
...(ngDevMode ? [{ debugName: "retakeButton" }] : /* istanbul ignore next */ []));
|
|
9801
|
+
this.useButton = computed(() => UploadWidgetUtils.createUseButton(this.texts()), /* @ts-ignore */
|
|
9802
|
+
...(ngDevMode ? [{ debugName: "useButton" }] : /* istanbul ignore next */ []));
|
|
9970
9803
|
}
|
|
9971
9804
|
ngAfterViewInit() {
|
|
9972
9805
|
if (this.deviceStart() === CaptureDeviceStart.AUTO) {
|
|
9973
|
-
this.
|
|
9806
|
+
afterNextRender(() => this.device.startCamera(), { injector: this.injector });
|
|
9974
9807
|
}
|
|
9975
9808
|
}
|
|
9976
|
-
|
|
9977
|
-
|
|
9809
|
+
ngOnDestroy() {
|
|
9810
|
+
this.device.release();
|
|
9811
|
+
}
|
|
9812
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CaptureCameraViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
9813
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: CaptureCameraViewComponent, isStandalone: true, selector: "smart-capture-camera-view", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: true, transformFunction: null }, deviceStart: { classPropertyName: "deviceStart", publicName: "deviceStart", isSignal: true, isRequired: true, transformFunction: null }, handover: { classPropertyName: "handover", publicName: "handover", isSignal: true, isRequired: true, transformFunction: null }, texts: { classPropertyName: "texts", publicName: "texts", isSignal: true, isRequired: true, transformFunction: null }, maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { captured: "captured" }, viewQueries: [{ propertyName: "video", first: true, predicate: ["video"], descendants: true, isSignal: true }, { propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, isSignal: true }, { propertyName: "actionButtonsTemplate", first: true, predicate: ["actionButtons"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"cameraStage\">\n <video #video autoplay [muted]=\"true\" [hidden]=\"device.isCaptured() || !device.isCameraActive()\"></video>\n <canvas #canvas [hidden]=\"!device.isCaptured() || device.isCameraActive()\"></canvas>\n @if (device.keptVideoUrl(); as videoUrl) {\n <video controls [src]=\"videoUrl\"></video>\n }\n @if (device.cameraError()) {\n <p class=\"message cameraErrorMessage\">{{ texts().cameraError }}</p>\n } @else if (capturedCaption(); as caption) {\n <p class=\"message capturedCaption\">{{ caption }}</p>\n } @else if (device.isRecording()) {\n <p class=\"message recordingMessage\">{{ recordingCaption() }}</p>\n } @else if (isSwitchOnHintShown()) {\n @if (mode() === 'VIDEO') {\n <p class=\"message\">{{ texts().switchOnForVideo }}</p>\n } @else {\n <p class=\"message\">{{ texts().switchOnCameraHint }}</p>\n }\n }\n @if (isCameraSwitchShown()) {\n <ui-action-button\n class=\"switchOnButton\"\n [descriptor]=\"switchOnButton()\"\n (actionClick)=\"device.startCamera()\"\n ></ui-action-button>\n }\n</div>\n\n<ng-template #actionButtons>\n @if (device.keptFiles()) {\n <ui-action-button\n class=\"retakeButton\"\n [descriptor]=\"retakeButton()\"\n (actionClick)=\"device.retakeCapture()\"\n ></ui-action-button>\n <ui-action-button\n class=\"useButton\"\n [descriptor]=\"useButton()\"\n (actionClick)=\"device.useCapture()\"\n ></ui-action-button>\n } @else if (mode() === 'VIDEO') {\n @if (device.isRecording()) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"device.stopRecording()\"\n ></ui-action-button>\n } @else {\n <ui-action-button\n class=\"startRecordingButton\"\n [disabled]=\"!device.isCameraActive()\"\n [descriptor]=\"startRecordingButton()\"\n (actionClick)=\"device.startRecording()\"\n ></ui-action-button>\n }\n } @else {\n <ui-action-button\n class=\"capturePhotoButton\"\n [disabled]=\"!device.isCameraActive()\"\n [descriptor]=\"captureButton()\"\n (actionClick)=\"device.capturePhoto()\"\n ></ui-action-button>\n }\n</ng-template>\n", styles: [":host{display:block;width:100%}.cameraStage{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px;--stage-background: var( --capture-stage-background, color-mix(in srgb, var(--primary-color) 30%, black) );background:var(--stage-background);color:var(--capture-stage-text, color-mix(in srgb, var(--primary-color) 10%, white))}.cameraStage video,.cameraStage canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}.cameraStage .switchOnButton ::ng-deep button{background:var(--color-surface)}.cameraStage .message{margin:0;text-align:center;color:inherit}.cameraStage .cameraErrorMessage{position:relative;color:color-mix(in srgb,var(--warn-color) 65%,white)}.cameraStage .recordingMessage,.cameraStage .capturedCaption{position:absolute;left:.5rem;top:.5rem;padding:.125rem .5rem;border-radius:.5rem;font-size:.75rem;font-weight:500}.cameraStage .recordingMessage{background:color-mix(in srgb,var(--stage-background) 67%,transparent);font-family:monospace}.cameraStage .capturedCaption{background:color-mix(in srgb,var(--success-color) 50%,black)}.cameraStage .recordingMessage:before{content:\"\";display:inline-block;width:.5rem;height:.5rem;margin-right:.3125rem;border-radius:50%;background:var(--warn-color)}\n"], dependencies: [{ kind: "component", type: UiActionButtonComponent, selector: "ui-action-button", inputs: ["disabled", "descriptor", "code", "identifier", "viewActivated", "tab", "rowState", "addedCssClass", "addBasicClasses", "iconPlaceholder", "chords"], outputs: ["actionClick", "actionDoubleClick"] }] }); }
|
|
9814
|
+
}
|
|
9815
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CaptureCameraViewComponent, decorators: [{
|
|
9816
|
+
type: Component,
|
|
9817
|
+
args: [{ selector: 'smart-capture-camera-view', imports: [UiActionButtonComponent], template: "<div class=\"cameraStage\">\n <video #video autoplay [muted]=\"true\" [hidden]=\"device.isCaptured() || !device.isCameraActive()\"></video>\n <canvas #canvas [hidden]=\"!device.isCaptured() || device.isCameraActive()\"></canvas>\n @if (device.keptVideoUrl(); as videoUrl) {\n <video controls [src]=\"videoUrl\"></video>\n }\n @if (device.cameraError()) {\n <p class=\"message cameraErrorMessage\">{{ texts().cameraError }}</p>\n } @else if (capturedCaption(); as caption) {\n <p class=\"message capturedCaption\">{{ caption }}</p>\n } @else if (device.isRecording()) {\n <p class=\"message recordingMessage\">{{ recordingCaption() }}</p>\n } @else if (isSwitchOnHintShown()) {\n @if (mode() === 'VIDEO') {\n <p class=\"message\">{{ texts().switchOnForVideo }}</p>\n } @else {\n <p class=\"message\">{{ texts().switchOnCameraHint }}</p>\n }\n }\n @if (isCameraSwitchShown()) {\n <ui-action-button\n class=\"switchOnButton\"\n [descriptor]=\"switchOnButton()\"\n (actionClick)=\"device.startCamera()\"\n ></ui-action-button>\n }\n</div>\n\n<ng-template #actionButtons>\n @if (device.keptFiles()) {\n <ui-action-button\n class=\"retakeButton\"\n [descriptor]=\"retakeButton()\"\n (actionClick)=\"device.retakeCapture()\"\n ></ui-action-button>\n <ui-action-button\n class=\"useButton\"\n [descriptor]=\"useButton()\"\n (actionClick)=\"device.useCapture()\"\n ></ui-action-button>\n } @else if (mode() === 'VIDEO') {\n @if (device.isRecording()) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"device.stopRecording()\"\n ></ui-action-button>\n } @else {\n <ui-action-button\n class=\"startRecordingButton\"\n [disabled]=\"!device.isCameraActive()\"\n [descriptor]=\"startRecordingButton()\"\n (actionClick)=\"device.startRecording()\"\n ></ui-action-button>\n }\n } @else {\n <ui-action-button\n class=\"capturePhotoButton\"\n [disabled]=\"!device.isCameraActive()\"\n [descriptor]=\"captureButton()\"\n (actionClick)=\"device.capturePhoto()\"\n ></ui-action-button>\n }\n</ng-template>\n", styles: [":host{display:block;width:100%}.cameraStage{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;aspect-ratio:16 / 9;overflow:hidden;border-radius:4px;--stage-background: var( --capture-stage-background, color-mix(in srgb, var(--primary-color) 30%, black) );background:var(--stage-background);color:var(--capture-stage-text, color-mix(in srgb, var(--primary-color) 10%, white))}.cameraStage video,.cameraStage canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}.cameraStage .switchOnButton ::ng-deep button{background:var(--color-surface)}.cameraStage .message{margin:0;text-align:center;color:inherit}.cameraStage .cameraErrorMessage{position:relative;color:color-mix(in srgb,var(--warn-color) 65%,white)}.cameraStage .recordingMessage,.cameraStage .capturedCaption{position:absolute;left:.5rem;top:.5rem;padding:.125rem .5rem;border-radius:.5rem;font-size:.75rem;font-weight:500}.cameraStage .recordingMessage{background:color-mix(in srgb,var(--stage-background) 67%,transparent);font-family:monospace}.cameraStage .capturedCaption{background:color-mix(in srgb,var(--success-color) 50%,black)}.cameraStage .recordingMessage:before{content:\"\";display:inline-block;width:.5rem;height:.5rem;margin-right:.3125rem;border-radius:50%;background:var(--warn-color)}\n"] }]
|
|
9818
|
+
}], propDecorators: { video: [{ type: i0.ViewChild, args: ['video', { isSignal: true }] }], canvas: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }], actionButtonsTemplate: [{ type: i0.ViewChild, args: ['actionButtons', { isSignal: true }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: true }] }], deviceStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "deviceStart", required: true }] }], handover: [{ type: i0.Input, args: [{ isSignal: true, alias: "handover", required: true }] }], texts: [{ type: i0.Input, args: [{ isSignal: true, alias: "texts", required: true }] }], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], captured: [{ type: i0.Output, args: ["captured"] }] } });
|
|
9819
|
+
|
|
9820
|
+
/** The microphone of one capture view: the record plugin, its start and the stopped recording.
|
|
9821
|
+
* The view calls release() when it closes. */
|
|
9822
|
+
class MicrophoneDevice {
|
|
9823
|
+
constructor(options) {
|
|
9824
|
+
this.options = options;
|
|
9825
|
+
this.isRecording = signal(false, /* @ts-ignore */
|
|
9826
|
+
...(ngDevMode ? [{ debugName: "isRecording" }] : /* istanbul ignore next */ []));
|
|
9827
|
+
this.recordedBlob = signal(null, /* @ts-ignore */
|
|
9828
|
+
...(ngDevMode ? [{ debugName: "recordedBlob" }] : /* istanbul ignore next */ []));
|
|
9829
|
+
this.time = signal('00:00', /* @ts-ignore */
|
|
9830
|
+
...(ngDevMode ? [{ debugName: "time" }] : /* istanbul ignore next */ []));
|
|
9831
|
+
this.isPlaying = signal(false, /* @ts-ignore */
|
|
9832
|
+
...(ngDevMode ? [{ debugName: "isPlaying" }] : /* istanbul ignore next */ []));
|
|
9833
|
+
/** Set when the browser gives no microphone, until the next start. */
|
|
9834
|
+
this.microphoneError = signal(false, /* @ts-ignore */
|
|
9835
|
+
...(ngDevMode ? [{ debugName: "microphoneError" }] : /* istanbul ignore next */ []));
|
|
9836
|
+
this.isWaveformOpen = signal(false, /* @ts-ignore */
|
|
9837
|
+
...(ngDevMode ? [{ debugName: "isWaveformOpen" }] : /* istanbul ignore next */ []));
|
|
9838
|
+
/** True while a host may not lose the capture by accident: the recording runs, its microphone
|
|
9839
|
+
* request included, or waits for the save. */
|
|
9840
|
+
this.isCaptureUnderWay = computed(() => this.isRecording() || !!this.recordedBlob(), /* @ts-ignore */
|
|
9841
|
+
...(ngDevMode ? [{ debugName: "isCaptureUnderWay" }] : /* istanbul ignore next */ []));
|
|
9842
|
+
/** True while the view shows the waveform band. */
|
|
9843
|
+
this.isWaveformShown = computed(() => this.isWaveformOpen() || this.isRecording() || !!this.recordedBlob(), /* @ts-ignore */
|
|
9844
|
+
...(ngDevMode ? [{ debugName: "isWaveformShown" }] : /* istanbul ignore next */ []));
|
|
9978
9845
|
}
|
|
9979
9846
|
async initializeWaveSurferIfNeeded() {
|
|
9980
|
-
|
|
9847
|
+
const waveform = this.options.getWaveformElement();
|
|
9848
|
+
if (this.wavesurfer || !waveform) {
|
|
9981
9849
|
return;
|
|
9982
9850
|
}
|
|
9983
9851
|
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
9984
9852
|
if (!this.pendingStart) {
|
|
9985
9853
|
return;
|
|
9986
9854
|
}
|
|
9987
|
-
|
|
9988
|
-
this.initWaveSurfer();
|
|
9855
|
+
waveform.style.padding = '0.1rem';
|
|
9856
|
+
this.initWaveSurfer(waveform);
|
|
9989
9857
|
}
|
|
9990
|
-
initWaveSurfer() {
|
|
9991
|
-
|
|
9992
|
-
// A host theme sets the token on the body, which the element inherits and html does not see.
|
|
9993
|
-
let primaryColor = getComputedStyle(this.actionsInHost() ? this.waveformRef.nativeElement : document.documentElement)
|
|
9858
|
+
initWaveSurfer(waveform) {
|
|
9859
|
+
const primaryColor = getComputedStyle(this.options.getThemeElement())
|
|
9994
9860
|
.getPropertyValue('--primary-color')
|
|
9995
9861
|
.trim();
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9862
|
+
const el = this.options.getButtonElement().querySelector('.sb4-primary');
|
|
9863
|
+
const remInPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
|
9864
|
+
const height = (el?.getBoundingClientRect().height ?? 0) - remInPx * 0.1;
|
|
9999
9865
|
this.wavesurfer = WaveSurfer.create({
|
|
10000
|
-
container:
|
|
9866
|
+
container: waveform,
|
|
10001
9867
|
waveColor: primaryColor,
|
|
10002
9868
|
progressColor: this.addAlphaToColor(primaryColor, 0.5),
|
|
10003
9869
|
height: height,
|
|
@@ -10016,31 +9882,31 @@ class VoiceRecordWidgetComponent {
|
|
|
10016
9882
|
}));
|
|
10017
9883
|
this.record = record;
|
|
10018
9884
|
this.wavesurfer.on('dblclick', () => {
|
|
10019
|
-
this.
|
|
9885
|
+
this.options.ngZone.run(() => {
|
|
10020
9886
|
this.wavesurfer?.play();
|
|
10021
|
-
this.isPlaying
|
|
9887
|
+
this.isPlaying.set(true);
|
|
10022
9888
|
});
|
|
10023
9889
|
});
|
|
10024
9890
|
this.record.on('record-end', (blob) => {
|
|
10025
9891
|
if (record !== this.record) {
|
|
10026
9892
|
return;
|
|
10027
9893
|
}
|
|
10028
|
-
this.
|
|
9894
|
+
this.options.ngZone.run(() => {
|
|
10029
9895
|
this.recordedBlob.set(blob);
|
|
10030
9896
|
this.isRecording.set(false);
|
|
10031
9897
|
});
|
|
10032
|
-
if (this.handover() === CaptureHandover.IMMEDIATE) {
|
|
9898
|
+
if (this.options.handover() === CaptureHandover.IMMEDIATE) {
|
|
10033
9899
|
this.saveRecording();
|
|
10034
9900
|
}
|
|
10035
9901
|
});
|
|
10036
9902
|
this.record.on('record-progress', (time) => {
|
|
10037
|
-
this.
|
|
10038
|
-
this.
|
|
9903
|
+
this.options.ngZone.run(() => {
|
|
9904
|
+
this.time.set(UploadWidgetUtils.formatDuration(time));
|
|
10039
9905
|
});
|
|
10040
9906
|
});
|
|
10041
9907
|
this.wavesurfer.on('finish', () => {
|
|
10042
|
-
this.
|
|
10043
|
-
this.isPlaying
|
|
9908
|
+
this.options.ngZone.run(() => {
|
|
9909
|
+
this.isPlaying.set(false);
|
|
10044
9910
|
});
|
|
10045
9911
|
});
|
|
10046
9912
|
}
|
|
@@ -10049,8 +9915,7 @@ class VoiceRecordWidgetComponent {
|
|
|
10049
9915
|
this.startRecording().catch(() => { });
|
|
10050
9916
|
}
|
|
10051
9917
|
async startRecording() {
|
|
10052
|
-
|
|
10053
|
-
if (!confirmed || this.isRecording())
|
|
9918
|
+
if (this.isRecording())
|
|
10054
9919
|
return;
|
|
10055
9920
|
const start = {};
|
|
10056
9921
|
this.pendingStart = start;
|
|
@@ -10062,13 +9927,13 @@ class VoiceRecordWidgetComponent {
|
|
|
10062
9927
|
const record = this.record;
|
|
10063
9928
|
if (!record)
|
|
10064
9929
|
throw new Error('Voice recorder plugin is not initialized.');
|
|
10065
|
-
this.
|
|
9930
|
+
this.options.ngZone.run(() => {
|
|
10066
9931
|
this.microphoneError.set(false);
|
|
10067
|
-
this.isWaveformOpen
|
|
9932
|
+
this.isWaveformOpen.set(true);
|
|
10068
9933
|
this.isRecording.set(true);
|
|
10069
|
-
this.isPlaying
|
|
9934
|
+
this.isPlaying.set(false);
|
|
10070
9935
|
this.recordedBlob.set(null);
|
|
10071
|
-
this.time
|
|
9936
|
+
this.time.set('00:00');
|
|
10072
9937
|
});
|
|
10073
9938
|
try {
|
|
10074
9939
|
// Force native mic permission first so it works with WKWebView
|
|
@@ -10091,34 +9956,21 @@ class VoiceRecordWidgetComponent {
|
|
|
10091
9956
|
}
|
|
10092
9957
|
console.error('Mic failed', error);
|
|
10093
9958
|
this.pendingStart = undefined;
|
|
10094
|
-
this.
|
|
9959
|
+
this.options.ngZone.run(() => {
|
|
10095
9960
|
this.isRecording.set(false);
|
|
10096
|
-
this.isWaveformOpen
|
|
9961
|
+
this.isWaveformOpen.set(!!this.recordedBlob());
|
|
10097
9962
|
this.microphoneError.set(true);
|
|
10098
9963
|
});
|
|
10099
9964
|
throw error;
|
|
10100
9965
|
}
|
|
10101
9966
|
}
|
|
10102
|
-
/**
|
|
10103
|
-
* Whether recording may start. Always `true` today: the branch below is where a "you
|
|
10104
|
-
* already have a recording — overwrite it?" confirmation was meant to go, and never did.
|
|
10105
|
-
* Kept, rather than inlined away, so the place stays visible; see A37.
|
|
10106
|
-
*/
|
|
10107
|
-
checkHasRecorded() {
|
|
10108
|
-
if (this.recordedBlob()) {
|
|
10109
|
-
}
|
|
10110
|
-
return true;
|
|
10111
|
-
}
|
|
10112
|
-
updateProgress(time) {
|
|
10113
|
-
this.time = UploadWidgetUtils.formatDuration(time);
|
|
10114
|
-
}
|
|
10115
9967
|
stopRecording() {
|
|
10116
9968
|
if (this.pendingStart) {
|
|
10117
9969
|
this.cancelPendingStart();
|
|
10118
9970
|
}
|
|
10119
9971
|
else if (this.record && this.isRecording()) {
|
|
10120
9972
|
this.record.stopRecording();
|
|
10121
|
-
this.
|
|
9973
|
+
this.options.ngZone.run(() => {
|
|
10122
9974
|
this.isRecording.set(false);
|
|
10123
9975
|
});
|
|
10124
9976
|
}
|
|
@@ -10127,9 +9979,9 @@ class VoiceRecordWidgetComponent {
|
|
|
10127
9979
|
cancelPendingStart() {
|
|
10128
9980
|
this.pendingStart = undefined;
|
|
10129
9981
|
this.destroyRecorder();
|
|
10130
|
-
this.
|
|
9982
|
+
this.options.ngZone.run(() => {
|
|
10131
9983
|
this.isRecording.set(false);
|
|
10132
|
-
this.isWaveformOpen
|
|
9984
|
+
this.isWaveformOpen.set(false);
|
|
10133
9985
|
});
|
|
10134
9986
|
}
|
|
10135
9987
|
/** The plugin's destroy alone closes a running microphone's AudioContext twice, which rejects;
|
|
@@ -10147,26 +9999,27 @@ class VoiceRecordWidgetComponent {
|
|
|
10147
9999
|
const recordedBlob = this.recordedBlob();
|
|
10148
10000
|
if (recordedBlob) {
|
|
10149
10001
|
const file = UploadWidgetUtils.createRecordingFile(recordedBlob, 'recording', 'audio/mp4');
|
|
10150
|
-
this.
|
|
10002
|
+
this.options.handOver(file);
|
|
10151
10003
|
// Destroy completely so next recording initializes fresh
|
|
10152
10004
|
this.destroyRecorder();
|
|
10153
|
-
this.
|
|
10005
|
+
this.options.ngZone.run(() => {
|
|
10154
10006
|
this.recordedBlob.set(null);
|
|
10155
|
-
this.time
|
|
10156
|
-
this.isPlaying
|
|
10157
|
-
this.isWaveformOpen
|
|
10007
|
+
this.time.set('00:00');
|
|
10008
|
+
this.isPlaying.set(false);
|
|
10009
|
+
this.isWaveformOpen.set(false);
|
|
10158
10010
|
});
|
|
10159
10011
|
}
|
|
10160
10012
|
}
|
|
10161
10013
|
replayRecording() {
|
|
10162
10014
|
if (this.recordedBlob()) {
|
|
10163
|
-
this.
|
|
10164
|
-
this.isPlaying
|
|
10015
|
+
this.options.ngZone.run(() => {
|
|
10016
|
+
this.isPlaying.update((isPlaying) => !isPlaying);
|
|
10165
10017
|
this.wavesurfer?.playPause();
|
|
10166
10018
|
});
|
|
10167
10019
|
}
|
|
10168
10020
|
}
|
|
10169
|
-
|
|
10021
|
+
/** Cancels a start under way and closes the microphone: the close of the view. */
|
|
10022
|
+
release() {
|
|
10170
10023
|
this.pendingStart = undefined;
|
|
10171
10024
|
this.destroyRecorder();
|
|
10172
10025
|
}
|
|
@@ -10191,25 +10044,67 @@ class VoiceRecordWidgetComponent {
|
|
|
10191
10044
|
}
|
|
10192
10045
|
return hexColor;
|
|
10193
10046
|
}
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10047
|
+
}
|
|
10048
|
+
|
|
10049
|
+
/** The recorder view of the Capture Dialog: the strip in the theme's colours, with its hints, and
|
|
10050
|
+
* the stop and save buttons in actionButtonsTemplate for the dialog's footer. */
|
|
10051
|
+
class CaptureRecorderViewComponent {
|
|
10052
|
+
constructor() {
|
|
10053
|
+
this.actionButtonsTemplate = viewChild('actionButtons', /* @ts-ignore */
|
|
10054
|
+
...(ngDevMode ? [{ debugName: "actionButtonsTemplate" }] : /* istanbul ignore next */ []));
|
|
10055
|
+
this.deviceStart = input.required(/* @ts-ignore */
|
|
10056
|
+
...(ngDevMode ? [{ debugName: "deviceStart" }] : /* istanbul ignore next */ []));
|
|
10057
|
+
this.handover = input.required(/* @ts-ignore */
|
|
10058
|
+
...(ngDevMode ? [{ debugName: "handover" }] : /* istanbul ignore next */ []));
|
|
10059
|
+
this.texts = input.required(/* @ts-ignore */
|
|
10060
|
+
...(ngDevMode ? [{ debugName: "texts" }] : /* istanbul ignore next */ []));
|
|
10061
|
+
this.recordingSaved = output();
|
|
10062
|
+
this.device = new MicrophoneDevice({
|
|
10063
|
+
handover: this.handover,
|
|
10064
|
+
ngZone: inject(NgZone),
|
|
10065
|
+
getWaveformElement: () => this.waveformRef?.nativeElement,
|
|
10066
|
+
getButtonElement: () => this.mainButton.nativeElement,
|
|
10067
|
+
// A host theme sets the token on the body, which the element inherits and html does not see.
|
|
10068
|
+
getThemeElement: () => this.waveformRef.nativeElement,
|
|
10069
|
+
handOver: (file) => this.recordingSaved.emit(file),
|
|
10070
|
+
});
|
|
10071
|
+
this.micButton = this.createIconButton('microphone', 'primary');
|
|
10072
|
+
/** Only a theme colour's name paints the icon. */
|
|
10073
|
+
this.stopButton = {
|
|
10074
|
+
...this.createIconButton('stop-circle', 'warn'),
|
|
10075
|
+
iconColor: 'warn',
|
|
10076
|
+
};
|
|
10077
|
+
this.playButton = this.createIconButton('play_circle', 'primary');
|
|
10078
|
+
this.stopPlaybackButton = this.createIconButton('stop_circle', 'warn');
|
|
10079
|
+
this.saveButton = computed(() => UploadWidgetUtils.createCaptureButton(this.texts().save, 'save'), /* @ts-ignore */
|
|
10080
|
+
...(ngDevMode ? [{ debugName: "saveButton" }] : /* istanbul ignore next */ []));
|
|
10081
|
+
this.stopRecordingButton = computed(() => UploadWidgetUtils.createStopRecordingButton(this.texts(), 'warn'), /* @ts-ignore */
|
|
10082
|
+
...(ngDevMode ? [{ debugName: "stopRecordingButton" }] : /* istanbul ignore next */ []));
|
|
10199
10083
|
}
|
|
10200
|
-
|
|
10201
|
-
|
|
10084
|
+
ngAfterViewInit() {
|
|
10085
|
+
if (this.deviceStart() === CaptureDeviceStart.AUTO) {
|
|
10086
|
+
this.device.requestRecording();
|
|
10087
|
+
}
|
|
10088
|
+
}
|
|
10089
|
+
ngOnDestroy() {
|
|
10090
|
+
this.device.release();
|
|
10091
|
+
}
|
|
10092
|
+
createIconButton(icon, color) {
|
|
10093
|
+
return UploadWidgetUtils.createCaptureButton('', icon, color, UiActionButtonType.ICON);
|
|
10094
|
+
}
|
|
10095
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CaptureRecorderViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10096
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: CaptureRecorderViewComponent, isStandalone: true, selector: "smart-capture-recorder-view", inputs: { deviceStart: { classPropertyName: "deviceStart", publicName: "deviceStart", isSignal: true, isRequired: true, transformFunction: null }, handover: { classPropertyName: "handover", publicName: "handover", isSignal: true, isRequired: true, transformFunction: null }, texts: { classPropertyName: "texts", publicName: "texts", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { recordingSaved: "recordingSaved" }, viewQueries: [{ propertyName: "actionButtonsTemplate", first: true, predicate: ["actionButtons"], descendants: true, isSignal: true }, { propertyName: "waveformRef", first: true, predicate: ["waveform"], descendants: true }, { propertyName: "mainButton", first: true, predicate: ["mainButton"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div\n class=\"recorderContainer direction-right\"\n [class.active]=\"device.isWaveformShown()\"\n [class.isRecording]=\"device.isRecording()\"\n [class.hasRecording]=\"device.recordedBlob()\"\n>\n <div class=\"recorderMain\">\n <div class=\"recorderToolbar\">\n @if (device.isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"stopButton\"\n (actionClick)=\"device.stopRecording()\"\n ></ui-action-button>\n <div class=\"progress\">{{ device.time() }}</div>\n } @else {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"micButton\"\n (actionClick)=\"device.requestRecording()\"\n ></ui-action-button>\n }\n\n @if (device.recordedBlob()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"device.isPlaying() ? stopPlaybackButton : playButton\"\n (actionClick)=\"device.replayRecording()\"\n ></ui-action-button>\n }\n </div>\n\n <div class=\"waveformBand\" [class.open]=\"device.isWaveformShown()\">\n <div class=\"waveformGrid\"></div>\n <div class=\"waveform\" #waveform></div>\n </div>\n </div>\n</div>\n@if (device.microphoneError()) {\n <p class=\"message\">{{ texts().microphoneError }}</p>\n <p class=\"hint\">{{ texts().microphoneRetryHint }}</p>\n}\n@if (device.recordedBlob()) {\n <p class=\"hint\">{{ texts().recordingHint }}</p>\n}\n\n<ng-template #actionButtons>\n @if (device.isRecording()) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"device.stopRecording()\"\n ></ui-action-button>\n }\n @if (device.recordedBlob()) {\n <ui-action-button\n class=\"saveRecordingButton\"\n [descriptor]=\"saveButton()\"\n (actionClick)=\"device.saveRecording()\"\n ></ui-action-button>\n }\n</ng-template>\n", styles: [":host{display:block;width:100%;min-width:0}.message{text-align:center;color:#6b7280}:host ::ng-deep .waveform{height:unset!important}.recorderContainer{display:flex;flex-direction:row;justify-content:space-between;width:100%;min-width:0;align-items:center;gap:.5rem}.active ::ng-deep .recorderToolbar{border:1px solid #e5e7eb;border-radius:.5rem}.isRecording ::ng-deep .recorderToolbar{padding-right:1rem}.recorderToolbar ::ng-deep .sb4-red,.recorderToolbar ::ng-deep .sb4-primary{border:1px solid #e5e7eb;box-shadow:unset}.active ::ng-deep .recorderToolbar ::ng-deep .sb4-red,.active ::ng-deep .recorderToolbar ::ng-deep .sb4-primary{border:unset}.mainButton ::ng-deep .sb4-red,.mainButton ::ng-deep .sb4-primary{padding:.5rem .75rem}.recorderMain{display:flex;align-items:center;min-width:0;flex:1}.direction-right .recorderMain{flex-direction:row}.direction-left .recorderMain{flex-direction:row-reverse}.waveform{width:100%;height:45px}.waveformBand{position:relative;display:grid;align-items:center;width:0;max-width:0;min-width:0;opacity:0;overflow:hidden;border-radius:5px;flex:1 1 0;transform:scaleX(.96);will-change:opacity,transform,margin;transition:max-width .22s ease,opacity .18s ease,margin .22s ease,transform .22s ease}.waveformBand.open{min-width:35px;max-width:100%;opacity:1;transform:scaleX(1)}.direction-right .waveformBand{margin-left:0}.direction-left .waveformBand{margin-right:0}.direction-right .waveformBand.open{margin-left:.45rem}.direction-left .waveformBand.open{margin-right:.45rem}.waveformGrid{position:absolute;inset:0;border:1px solid #e5e7eb;border-radius:.5rem;background:repeating-linear-gradient(to right,rgba(148,163,184,.35) 0,rgba(148,163,184,.35) 2px,transparent 2px,transparent 8px),linear-gradient(to bottom,#94a3b814,#94a3b805);pointer-events:none}.recorderToolbar{background-color:transparent;display:flex;flex-direction:row;align-items:center}.progress{align-content:center;font-size:1rem;height:100%;color:#6b7280}.hasRecording ::ng-deep .recorderToolbar ui-action-button{border-right:1px solid #e5e7eb}.hasRecording ::ng-deep .recorderToolbar ui-action-button:last-child{border-right:unset}\n", ":host .message{color:var(--warn-color)}:host .hint{margin:.5rem 0 0;text-align:center;font-size:.75rem;color:var(--color-text-muted)}:host .progress{color:var(--color-text-muted)}:host .isRecording .progress:before{content:\"\";display:inline-block;width:.5rem;height:.5rem;margin-right:.3125rem;border-radius:50%;background:var(--warn-color)}:host .active ::ng-deep .recorderToolbar{border-color:var(--color-border)}:host .waveformGrid{border-color:var(--color-border);background:repeating-linear-gradient(to right,color-mix(in srgb,var(--color-text-muted) 35%,transparent) 0,color-mix(in srgb,var(--color-text-muted) 35%,transparent) 2px,transparent 2px,transparent 8px),linear-gradient(to bottom,color-mix(in srgb,var(--color-text-muted) 8%,transparent),color-mix(in srgb,var(--color-text-muted) 2%,transparent))}:host .recorderToolbar ::ng-deep .sb4-warn,:host .recorderToolbar ::ng-deep .sb4-primary,:host .recorderToolbar ::ng-deep ui-action-button{border:unset}:host .mainButton ::ng-deep .sb4-warn{padding:.5rem .75rem}\n"], dependencies: [{ kind: "component", type: UiActionButtonComponent, selector: "ui-action-button", inputs: ["disabled", "descriptor", "code", "identifier", "viewActivated", "tab", "rowState", "addedCssClass", "addBasicClasses", "iconPlaceholder", "chords"], outputs: ["actionClick", "actionDoubleClick"] }] }); }
|
|
10202
10097
|
}
|
|
10203
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type:
|
|
10098
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CaptureRecorderViewComponent, decorators: [{
|
|
10204
10099
|
type: Component,
|
|
10205
|
-
args: [{ selector: '
|
|
10100
|
+
args: [{ selector: 'smart-capture-recorder-view', imports: [UiActionButtonComponent], template: "<div\n class=\"recorderContainer direction-right\"\n [class.active]=\"device.isWaveformShown()\"\n [class.isRecording]=\"device.isRecording()\"\n [class.hasRecording]=\"device.recordedBlob()\"\n>\n <div class=\"recorderMain\">\n <div class=\"recorderToolbar\">\n @if (device.isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"stopButton\"\n (actionClick)=\"device.stopRecording()\"\n ></ui-action-button>\n <div class=\"progress\">{{ device.time() }}</div>\n } @else {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"micButton\"\n (actionClick)=\"device.requestRecording()\"\n ></ui-action-button>\n }\n\n @if (device.recordedBlob()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"device.isPlaying() ? stopPlaybackButton : playButton\"\n (actionClick)=\"device.replayRecording()\"\n ></ui-action-button>\n }\n </div>\n\n <div class=\"waveformBand\" [class.open]=\"device.isWaveformShown()\">\n <div class=\"waveformGrid\"></div>\n <div class=\"waveform\" #waveform></div>\n </div>\n </div>\n</div>\n@if (device.microphoneError()) {\n <p class=\"message\">{{ texts().microphoneError }}</p>\n <p class=\"hint\">{{ texts().microphoneRetryHint }}</p>\n}\n@if (device.recordedBlob()) {\n <p class=\"hint\">{{ texts().recordingHint }}</p>\n}\n\n<ng-template #actionButtons>\n @if (device.isRecording()) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"device.stopRecording()\"\n ></ui-action-button>\n }\n @if (device.recordedBlob()) {\n <ui-action-button\n class=\"saveRecordingButton\"\n [descriptor]=\"saveButton()\"\n (actionClick)=\"device.saveRecording()\"\n ></ui-action-button>\n }\n</ng-template>\n", styles: [":host{display:block;width:100%;min-width:0}.message{text-align:center;color:#6b7280}:host ::ng-deep .waveform{height:unset!important}.recorderContainer{display:flex;flex-direction:row;justify-content:space-between;width:100%;min-width:0;align-items:center;gap:.5rem}.active ::ng-deep .recorderToolbar{border:1px solid #e5e7eb;border-radius:.5rem}.isRecording ::ng-deep .recorderToolbar{padding-right:1rem}.recorderToolbar ::ng-deep .sb4-red,.recorderToolbar ::ng-deep .sb4-primary{border:1px solid #e5e7eb;box-shadow:unset}.active ::ng-deep .recorderToolbar ::ng-deep .sb4-red,.active ::ng-deep .recorderToolbar ::ng-deep .sb4-primary{border:unset}.mainButton ::ng-deep .sb4-red,.mainButton ::ng-deep .sb4-primary{padding:.5rem .75rem}.recorderMain{display:flex;align-items:center;min-width:0;flex:1}.direction-right .recorderMain{flex-direction:row}.direction-left .recorderMain{flex-direction:row-reverse}.waveform{width:100%;height:45px}.waveformBand{position:relative;display:grid;align-items:center;width:0;max-width:0;min-width:0;opacity:0;overflow:hidden;border-radius:5px;flex:1 1 0;transform:scaleX(.96);will-change:opacity,transform,margin;transition:max-width .22s ease,opacity .18s ease,margin .22s ease,transform .22s ease}.waveformBand.open{min-width:35px;max-width:100%;opacity:1;transform:scaleX(1)}.direction-right .waveformBand{margin-left:0}.direction-left .waveformBand{margin-right:0}.direction-right .waveformBand.open{margin-left:.45rem}.direction-left .waveformBand.open{margin-right:.45rem}.waveformGrid{position:absolute;inset:0;border:1px solid #e5e7eb;border-radius:.5rem;background:repeating-linear-gradient(to right,rgba(148,163,184,.35) 0,rgba(148,163,184,.35) 2px,transparent 2px,transparent 8px),linear-gradient(to bottom,#94a3b814,#94a3b805);pointer-events:none}.recorderToolbar{background-color:transparent;display:flex;flex-direction:row;align-items:center}.progress{align-content:center;font-size:1rem;height:100%;color:#6b7280}.hasRecording ::ng-deep .recorderToolbar ui-action-button{border-right:1px solid #e5e7eb}.hasRecording ::ng-deep .recorderToolbar ui-action-button:last-child{border-right:unset}\n", ":host .message{color:var(--warn-color)}:host .hint{margin:.5rem 0 0;text-align:center;font-size:.75rem;color:var(--color-text-muted)}:host .progress{color:var(--color-text-muted)}:host .isRecording .progress:before{content:\"\";display:inline-block;width:.5rem;height:.5rem;margin-right:.3125rem;border-radius:50%;background:var(--warn-color)}:host .active ::ng-deep .recorderToolbar{border-color:var(--color-border)}:host .waveformGrid{border-color:var(--color-border);background:repeating-linear-gradient(to right,color-mix(in srgb,var(--color-text-muted) 35%,transparent) 0,color-mix(in srgb,var(--color-text-muted) 35%,transparent) 2px,transparent 2px,transparent 8px),linear-gradient(to bottom,color-mix(in srgb,var(--color-text-muted) 8%,transparent),color-mix(in srgb,var(--color-text-muted) 2%,transparent))}:host .recorderToolbar ::ng-deep .sb4-warn,:host .recorderToolbar ::ng-deep .sb4-primary,:host .recorderToolbar ::ng-deep ui-action-button{border:unset}:host .mainButton ::ng-deep .sb4-warn{padding:.5rem .75rem}\n"] }]
|
|
10206
10101
|
}], propDecorators: { waveformRef: [{
|
|
10207
10102
|
type: ViewChild,
|
|
10208
10103
|
args: ['waveform']
|
|
10209
10104
|
}], mainButton: [{
|
|
10210
10105
|
type: ViewChild,
|
|
10211
10106
|
args: ['mainButton', { read: ElementRef }]
|
|
10212
|
-
}], actionButtonsTemplate: [{ type: i0.ViewChild, args: ['actionButtons', { isSignal: true }] }],
|
|
10107
|
+
}], actionButtonsTemplate: [{ type: i0.ViewChild, args: ['actionButtons', { isSignal: true }] }], deviceStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "deviceStart", required: true }] }], handover: [{ type: i0.Input, args: [{ isSignal: true, alias: "handover", required: true }] }], texts: [{ type: i0.Input, args: [{ isSignal: true, alias: "texts", required: true }] }], recordingSaved: [{ type: i0.Output, args: ["recordingSaved"] }] } });
|
|
10213
10108
|
|
|
10214
10109
|
/** The Capture Dialog of a File Field: closes with the files of a use or a save, else empty. */
|
|
10215
10110
|
class CaptureDialogComponent {
|
|
@@ -10218,14 +10113,14 @@ class CaptureDialogComponent {
|
|
|
10218
10113
|
this.data = inject(MAT_DIALOG_DATA);
|
|
10219
10114
|
this.isMicrophone = this.data.source === FileUploaderSource.MICROPHONE;
|
|
10220
10115
|
this.cameraMode = this.data.source === FileUploaderSource.CAMERA_VIDEO ? 'VIDEO' : 'PHOTO';
|
|
10221
|
-
this.
|
|
10222
|
-
...(ngDevMode ? [{ debugName: "
|
|
10223
|
-
this.
|
|
10224
|
-
...(ngDevMode ? [{ debugName: "
|
|
10225
|
-
this.
|
|
10226
|
-
...(ngDevMode ? [{ debugName: "
|
|
10227
|
-
/** The
|
|
10228
|
-
this.actionButtons = computed(() => this.
|
|
10116
|
+
this.cameraView = viewChild(CaptureCameraViewComponent, /* @ts-ignore */
|
|
10117
|
+
...(ngDevMode ? [{ debugName: "cameraView" }] : /* istanbul ignore next */ []));
|
|
10118
|
+
this.recorderView = viewChild(CaptureRecorderViewComponent, /* @ts-ignore */
|
|
10119
|
+
...(ngDevMode ? [{ debugName: "recorderView" }] : /* istanbul ignore next */ []));
|
|
10120
|
+
this.captureView = computed(() => this.cameraView() ?? this.recorderView(), /* @ts-ignore */
|
|
10121
|
+
...(ngDevMode ? [{ debugName: "captureView" }] : /* istanbul ignore next */ []));
|
|
10122
|
+
/** The view's buttons go to the footer of the dialog. */
|
|
10123
|
+
this.actionButtons = computed(() => this.captureView()?.actionButtonsTemplate() ?? null, /* @ts-ignore */
|
|
10229
10124
|
...(ngDevMode ? [{ debugName: "actionButtons" }] : /* istanbul ignore next */ []));
|
|
10230
10125
|
/** The recorder waits for the open: it reads the waveform's height from its rendered button. */
|
|
10231
10126
|
this.isOpened = signal(false, /* @ts-ignore */
|
|
@@ -10243,7 +10138,7 @@ class CaptureDialogComponent {
|
|
|
10243
10138
|
color: 'primary',
|
|
10244
10139
|
};
|
|
10245
10140
|
effect(() => {
|
|
10246
|
-
this.dialogRef.disableClose = this.
|
|
10141
|
+
this.dialogRef.disableClose = this.captureView()?.device.isCaptureUnderWay() ?? false;
|
|
10247
10142
|
});
|
|
10248
10143
|
this.dialogRef.afterOpened().subscribe(() => this.isOpened.set(true));
|
|
10249
10144
|
// disableClose stops the keyboard too, but only the backdrop is meant to wait for the capture.
|
|
@@ -10253,7 +10148,7 @@ class CaptureDialogComponent {
|
|
|
10253
10148
|
}
|
|
10254
10149
|
});
|
|
10255
10150
|
}
|
|
10256
|
-
/** The close destroys the capture
|
|
10151
|
+
/** The close destroys the capture view, which stops the device. */
|
|
10257
10152
|
discardCapture() {
|
|
10258
10153
|
this.dialogRef.close();
|
|
10259
10154
|
}
|
|
@@ -10261,18 +10156,18 @@ class CaptureDialogComponent {
|
|
|
10261
10156
|
this.dialogRef.close(files);
|
|
10262
10157
|
}
|
|
10263
10158
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CaptureDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10264
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: CaptureDialogComponent, isStandalone: true, selector: "smart-capture-dialog", viewQueries: [{ propertyName: "
|
|
10159
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: CaptureDialogComponent, isStandalone: true, selector: "smart-capture-dialog", viewQueries: [{ propertyName: "cameraView", first: true, predicate: CaptureCameraViewComponent, descendants: true, isSignal: true }, { propertyName: "recorderView", first: true, predicate: CaptureRecorderViewComponent, descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"dialog-container captureDialog\">\n <div class=\"dialog-header\">\n <h2 class=\"dialog-title\">{{ data.content.title }}</h2>\n <ui-action-button\n class=\"closeCaptureButton\"\n [descriptor]=\"closeButton\"\n (actionClick)=\"discardCapture()\"\n ></ui-action-button>\n </div>\n\n <mat-dialog-content class=\"dialog-content\">\n @if (isMicrophone) {\n <div class=\"recorderSlot\">\n @if (isOpened()) {\n <smart-capture-recorder-view\n [deviceStart]=\"data.settings.deviceStart\"\n [handover]=\"data.settings.handover\"\n [texts]=\"data.texts\"\n (recordingSaved)=\"handOverCapture([$event])\"\n ></smart-capture-recorder-view>\n }\n </div>\n } @else {\n <smart-capture-camera-view\n [mode]=\"cameraMode\"\n [deviceStart]=\"data.settings.deviceStart\"\n [handover]=\"data.settings.handover\"\n [texts]=\"data.texts\"\n [maxFileSize]=\"data.maxFileSize\"\n (captured)=\"handOverCapture($event)\"\n ></smart-capture-camera-view>\n }\n </mat-dialog-content>\n\n <div class=\"dialog-footer\">\n <ui-action-button\n class=\"cancelCaptureButton\"\n [descriptor]=\"cancelButton\"\n (actionClick)=\"discardCapture()\"\n ></ui-action-button>\n <ng-container [ngTemplateOutlet]=\"actionButtons()\" />\n </div>\n</div>\n", styles: [".dialog-container{display:flex;flex-direction:column;--sb4-dialog-default-padding: .5rem;--mat-dialog-content-padding: var(--sb4-dialog-default-padding)}.dialog-header{display:flex;align-items:center;gap:var(--sb4-dialog-default-padding);padding:var(--sb4-dialog-default-padding)}.mat-mdc-dialog-content{max-height:var(--sb4-dialog-default-max-height, 80vh);max-width:var(--sb4-dialog-default-max-width, unset);min-width:var(--sb4-dialog-default-min-width, unset)}.dialog-title{flex:1;margin:0}.dialog-footer{padding:var(--sb4-dialog-default-padding)}\n", ".captureDialog{--sb4-dialog-default-padding: 1rem}.captureDialog .dialog-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:.5rem}.closeCaptureButton ::ng-deep button{width:40px;min-width:40px;height:40px;min-height:40px;padding:0;border-radius:50%}.captureDialog ::ng-deep .stopRecordingButton{--text-color: color-mix(in srgb, var(--warn-color) 5%, white)}.recorderSlot{min-height:3.25rem}\n"], dependencies: [{ kind: "component", type: CaptureCameraViewComponent, selector: "smart-capture-camera-view", inputs: ["mode", "deviceStart", "handover", "texts", "maxFileSize"], outputs: ["captured"] }, { kind: "component", type: CaptureRecorderViewComponent, selector: "smart-capture-recorder-view", inputs: ["deviceStart", "handover", "texts"], outputs: ["recordingSaved"] }, { kind: "directive", type: MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: UiActionButtonComponent, selector: "ui-action-button", inputs: ["disabled", "descriptor", "code", "identifier", "viewActivated", "tab", "rowState", "addedCssClass", "addBasicClasses", "iconPlaceholder", "chords"], outputs: ["actionClick", "actionDoubleClick"] }] }); }
|
|
10265
10160
|
}
|
|
10266
10161
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CaptureDialogComponent, decorators: [{
|
|
10267
10162
|
type: Component,
|
|
10268
10163
|
args: [{ selector: 'smart-capture-dialog', imports: [
|
|
10164
|
+
CaptureCameraViewComponent,
|
|
10165
|
+
CaptureRecorderViewComponent,
|
|
10269
10166
|
MatDialogContent,
|
|
10270
10167
|
NgTemplateOutlet,
|
|
10271
|
-
PhotoCaptureWidgetComponent,
|
|
10272
10168
|
UiActionButtonComponent,
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
}], ctorParameters: () => [], propDecorators: { cameraWidget: [{ type: i0.ViewChild, args: [i0.forwardRef(() => PhotoCaptureWidgetComponent), { isSignal: true }] }], recorderWidget: [{ type: i0.ViewChild, args: [i0.forwardRef(() => VoiceRecordWidgetComponent), { isSignal: true }] }] } });
|
|
10169
|
+
], template: "<div class=\"dialog-container captureDialog\">\n <div class=\"dialog-header\">\n <h2 class=\"dialog-title\">{{ data.content.title }}</h2>\n <ui-action-button\n class=\"closeCaptureButton\"\n [descriptor]=\"closeButton\"\n (actionClick)=\"discardCapture()\"\n ></ui-action-button>\n </div>\n\n <mat-dialog-content class=\"dialog-content\">\n @if (isMicrophone) {\n <div class=\"recorderSlot\">\n @if (isOpened()) {\n <smart-capture-recorder-view\n [deviceStart]=\"data.settings.deviceStart\"\n [handover]=\"data.settings.handover\"\n [texts]=\"data.texts\"\n (recordingSaved)=\"handOverCapture([$event])\"\n ></smart-capture-recorder-view>\n }\n </div>\n } @else {\n <smart-capture-camera-view\n [mode]=\"cameraMode\"\n [deviceStart]=\"data.settings.deviceStart\"\n [handover]=\"data.settings.handover\"\n [texts]=\"data.texts\"\n [maxFileSize]=\"data.maxFileSize\"\n (captured)=\"handOverCapture($event)\"\n ></smart-capture-camera-view>\n }\n </mat-dialog-content>\n\n <div class=\"dialog-footer\">\n <ui-action-button\n class=\"cancelCaptureButton\"\n [descriptor]=\"cancelButton\"\n (actionClick)=\"discardCapture()\"\n ></ui-action-button>\n <ng-container [ngTemplateOutlet]=\"actionButtons()\" />\n </div>\n</div>\n", styles: [".dialog-container{display:flex;flex-direction:column;--sb4-dialog-default-padding: .5rem;--mat-dialog-content-padding: var(--sb4-dialog-default-padding)}.dialog-header{display:flex;align-items:center;gap:var(--sb4-dialog-default-padding);padding:var(--sb4-dialog-default-padding)}.mat-mdc-dialog-content{max-height:var(--sb4-dialog-default-max-height, 80vh);max-width:var(--sb4-dialog-default-max-width, unset);min-width:var(--sb4-dialog-default-min-width, unset)}.dialog-title{flex:1;margin:0}.dialog-footer{padding:var(--sb4-dialog-default-padding)}\n", ".captureDialog{--sb4-dialog-default-padding: 1rem}.captureDialog .dialog-footer{display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;gap:.5rem}.closeCaptureButton ::ng-deep button{width:40px;min-width:40px;height:40px;min-height:40px;padding:0;border-radius:50%}.captureDialog ::ng-deep .stopRecordingButton{--text-color: color-mix(in srgb, var(--warn-color) 5%, white)}.recorderSlot{min-height:3.25rem}\n"] }]
|
|
10170
|
+
}], ctorParameters: () => [], propDecorators: { cameraView: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CaptureCameraViewComponent), { isSignal: true }] }], recorderView: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CaptureRecorderViewComponent), { isSignal: true }] }] } });
|
|
10276
10171
|
|
|
10277
10172
|
class CaptureDialogService extends SmartdialogService {
|
|
10278
10173
|
constructor() {
|
|
@@ -10294,6 +10189,239 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
10294
10189
|
args: [{ providedIn: 'root' }]
|
|
10295
10190
|
}], ctorParameters: () => [] });
|
|
10296
10191
|
|
|
10192
|
+
/** The inline camera panel of the upload zone; a phone opens its own camera instead. */
|
|
10193
|
+
class PhotoCaptureWidgetComponent {
|
|
10194
|
+
constructor() {
|
|
10195
|
+
this.injector = inject(Injector);
|
|
10196
|
+
this.maxFileSize = input(/* @ts-ignore */
|
|
10197
|
+
...(ngDevMode ? [undefined, { debugName: "maxFileSize" }] : /* istanbul ignore next */ []));
|
|
10198
|
+
/** PHOTO takes a picture; VIDEO records a video with the webcam or picks one on a desktop. */
|
|
10199
|
+
this.mode = input('PHOTO', /* @ts-ignore */
|
|
10200
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
10201
|
+
/** True when the zone's capture row carries the button: the widget then opens only by
|
|
10202
|
+
* openCapture(). */
|
|
10203
|
+
this.externalTrigger = input(false, /* @ts-ignore */
|
|
10204
|
+
...(ngDevMode ? [{ debugName: "externalTrigger" }] : /* istanbul ignore next */ []));
|
|
10205
|
+
/** The zone's texts; the buttons follow them. */
|
|
10206
|
+
this.texts = input(UPLOAD_ZONE_DEFAULT_TEXTS, /* @ts-ignore */
|
|
10207
|
+
...(ngDevMode ? [{ debugName: "texts" }] : /* istanbul ignore next */ []));
|
|
10208
|
+
/** AUTO asks for the camera when the widget opens, MANUAL at the switch-on button. */
|
|
10209
|
+
this.deviceStart = input(CaptureDeviceStart.MANUAL, /* @ts-ignore */
|
|
10210
|
+
...(ngDevMode ? [{ debugName: "deviceStart" }] : /* istanbul ignore next */ []));
|
|
10211
|
+
/** CONFIRM keeps a capture for retake or use, IMMEDIATE hands it over at the take or the stop. */
|
|
10212
|
+
this.handover = input(CaptureHandover.IMMEDIATE, /* @ts-ignore */
|
|
10213
|
+
...(ngDevMode ? [{ debugName: "handover" }] : /* istanbul ignore next */ []));
|
|
10214
|
+
this.photoCaptured = output();
|
|
10215
|
+
this.device = new CameraDevice({
|
|
10216
|
+
mode: this.mode,
|
|
10217
|
+
deviceStart: this.deviceStart,
|
|
10218
|
+
handover: this.handover,
|
|
10219
|
+
maxFileSize: this.maxFileSize,
|
|
10220
|
+
getVideoElement: () => this.videoRef.nativeElement,
|
|
10221
|
+
getCanvasElement: () => this.canvasRef.nativeElement,
|
|
10222
|
+
handOver: (files) => this.photoCaptured.emit(files),
|
|
10223
|
+
});
|
|
10224
|
+
/** The desktop webcam panel of an externally triggered photo widget, opened by openCapture(). */
|
|
10225
|
+
this.isPanelOpen = signal(false, /* @ts-ignore */
|
|
10226
|
+
...(ngDevMode ? [{ debugName: "isPanelOpen" }] : /* istanbul ignore next */ []));
|
|
10227
|
+
/** Under AUTO the switch returns when the camera is off, so a second shot stays possible. */
|
|
10228
|
+
this.isCameraSwitchShown = computed(() => !this.device.keptFiles() &&
|
|
10229
|
+
(this.deviceStart() === CaptureDeviceStart.MANUAL ||
|
|
10230
|
+
!(this.device.isCameraActive() || this.device.isCameraStarting())), /* @ts-ignore */
|
|
10231
|
+
...(ngDevMode ? [{ debugName: "isCameraSwitchShown" }] : /* istanbul ignore next */ []));
|
|
10232
|
+
this.isSwitchOnHintShown = computed(() => this.isCameraSwitchShown() && !this.device.isCameraActive() && !this.device.isCaptured(), /* @ts-ignore */
|
|
10233
|
+
...(ngDevMode ? [{ debugName: "isSwitchOnHintShown" }] : /* istanbul ignore next */ []));
|
|
10234
|
+
this.toggleCameraButton = computed(() => UploadWidgetUtils.createCaptureButton(this.device.isCameraActive() ? this.texts().cameraOff : this.texts().cameraOn, 'power_settings_new', this.device.isCameraActive() ? 'red' : 'primary'), /* @ts-ignore */
|
|
10235
|
+
...(ngDevMode ? [{ debugName: "toggleCameraButton" }] : /* istanbul ignore next */ []));
|
|
10236
|
+
this.captureButton = computed(() => UploadWidgetUtils.createTakePictureButton(this.texts()), /* @ts-ignore */
|
|
10237
|
+
...(ngDevMode ? [{ debugName: "captureButton" }] : /* istanbul ignore next */ []));
|
|
10238
|
+
this.startRecordingButton = computed(() => UploadWidgetUtils.createStartRecordingButton(this.texts()), /* @ts-ignore */
|
|
10239
|
+
...(ngDevMode ? [{ debugName: "startRecordingButton" }] : /* istanbul ignore next */ []));
|
|
10240
|
+
this.stopRecordingButton = computed(() => UploadWidgetUtils.createStopRecordingButton(this.texts(), 'red'), /* @ts-ignore */
|
|
10241
|
+
...(ngDevMode ? [{ debugName: "stopRecordingButton" }] : /* istanbul ignore next */ []));
|
|
10242
|
+
this.pickVideoButton = computed(() => UploadWidgetUtils.createCaptureButton(this.texts().pickVideo, 'video_library'), /* @ts-ignore */
|
|
10243
|
+
...(ngDevMode ? [{ debugName: "pickVideoButton" }] : /* istanbul ignore next */ []));
|
|
10244
|
+
this.retakeButton = computed(() => UploadWidgetUtils.createRetakeButton(this.texts()), /* @ts-ignore */
|
|
10245
|
+
...(ngDevMode ? [{ debugName: "retakeButton" }] : /* istanbul ignore next */ []));
|
|
10246
|
+
this.useButton = computed(() => UploadWidgetUtils.createUseButton(this.texts()), /* @ts-ignore */
|
|
10247
|
+
...(ngDevMode ? [{ debugName: "useButton" }] : /* istanbul ignore next */ []));
|
|
10248
|
+
}
|
|
10249
|
+
isMobile() {
|
|
10250
|
+
return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
10251
|
+
}
|
|
10252
|
+
/** A phone opens its own camera, a desktop the webcam panel. */
|
|
10253
|
+
openCapture() {
|
|
10254
|
+
if (this.isMobile()) {
|
|
10255
|
+
this.fileInputRef.nativeElement.click();
|
|
10256
|
+
return;
|
|
10257
|
+
}
|
|
10258
|
+
if (this.isPanelOpen()) {
|
|
10259
|
+
this.device.release();
|
|
10260
|
+
}
|
|
10261
|
+
else {
|
|
10262
|
+
this.startCameraAfterRender();
|
|
10263
|
+
}
|
|
10264
|
+
this.isPanelOpen.update((open) => !open);
|
|
10265
|
+
}
|
|
10266
|
+
ngAfterViewInit() {
|
|
10267
|
+
if (!this.externalTrigger()) {
|
|
10268
|
+
this.startCameraAfterRender();
|
|
10269
|
+
}
|
|
10270
|
+
}
|
|
10271
|
+
ngOnDestroy() {
|
|
10272
|
+
this.device.release();
|
|
10273
|
+
}
|
|
10274
|
+
/** AUTO waits for the render: the stream goes into the panel's video element. */
|
|
10275
|
+
startCameraAfterRender() {
|
|
10276
|
+
if (this.deviceStart() !== CaptureDeviceStart.AUTO || this.isMobile()) {
|
|
10277
|
+
return;
|
|
10278
|
+
}
|
|
10279
|
+
afterNextRender(() => {
|
|
10280
|
+
if (!this.externalTrigger() || this.isPanelOpen()) {
|
|
10281
|
+
this.device.startCamera();
|
|
10282
|
+
}
|
|
10283
|
+
}, { injector: this.injector });
|
|
10284
|
+
}
|
|
10285
|
+
getPhotoDataURL() {
|
|
10286
|
+
return this.canvasRef.nativeElement.toDataURL('image/png');
|
|
10287
|
+
}
|
|
10288
|
+
async onFileSelected(event) {
|
|
10289
|
+
const input = event.target;
|
|
10290
|
+
if (!input.files || input.files.length === 0)
|
|
10291
|
+
return;
|
|
10292
|
+
const maxSize = this.maxFileSize();
|
|
10293
|
+
const filesArray = Array.from(input.files);
|
|
10294
|
+
const processedFiles = [];
|
|
10295
|
+
for (const file of filesArray) {
|
|
10296
|
+
if (maxSize && file.size > maxSize && file.type.startsWith('image/')) {
|
|
10297
|
+
try {
|
|
10298
|
+
const compressed = await UploadWidgetUtils.compressImageFile(file, maxSize);
|
|
10299
|
+
processedFiles.push(compressed);
|
|
10300
|
+
}
|
|
10301
|
+
catch {
|
|
10302
|
+
processedFiles.push(file);
|
|
10303
|
+
}
|
|
10304
|
+
}
|
|
10305
|
+
else {
|
|
10306
|
+
processedFiles.push(file);
|
|
10307
|
+
}
|
|
10308
|
+
}
|
|
10309
|
+
this.photoCaptured.emit(processedFiles);
|
|
10310
|
+
}
|
|
10311
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PhotoCaptureWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10312
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: PhotoCaptureWidgetComponent, isStandalone: true, selector: "photo-capture-widget", inputs: { maxFileSize: { classPropertyName: "maxFileSize", publicName: "maxFileSize", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, externalTrigger: { classPropertyName: "externalTrigger", publicName: "externalTrigger", isSignal: true, isRequired: false, transformFunction: null }, texts: { classPropertyName: "texts", publicName: "texts", isSignal: true, isRequired: false, transformFunction: null }, deviceStart: { classPropertyName: "deviceStart", publicName: "deviceStart", isSignal: true, isRequired: false, transformFunction: null }, handover: { classPropertyName: "handover", publicName: "handover", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { photoCaptured: "photoCaptured" }, viewQueries: [{ propertyName: "videoRef", first: true, predicate: ["video"], descendants: true }, { propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true }, { propertyName: "fileInputRef", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: "@if (isMobile()) {\n <input\n #fileInput\n type=\"file\"\n [accept]=\"mode() === 'VIDEO' ? 'video/*' : 'image/*'\"\n capture=\"environment\"\n (change)=\"onFileSelected($event)\"\n style=\"display: none\"\n />\n @if (!externalTrigger()) {\n <div class=\"photoWidgetContainer\">\n <div class=\"controls\">\n <ui-action-button [descriptor]=\"captureButton()\" (actionClick)=\"fileInput.click()\">\n </ui-action-button>\n </div>\n </div>\n }\n} @else if (!externalTrigger() || isPanelOpen()) {\n <div class=\"photoWidgetContainer\">\n <video #video autoplay [muted]=\"true\" [hidden]=\"device.isCaptured() || !device.isCameraActive()\"></video>\n <canvas #canvas [hidden]=\"!device.isCaptured() || device.isCameraActive()\"></canvas>\n @if (device.keptVideoUrl(); as videoUrl) {\n <video controls [src]=\"videoUrl\"></video>\n }\n @if (device.cameraError()) {\n <p class=\"message\">{{ texts().cameraError }}</p>\n } @else if (device.isRecording()) {\n <p class=\"message\">{{ texts().recording }}</p>\n } @else if (isSwitchOnHintShown()) {\n @if (mode() === 'VIDEO') {\n <p class=\"message\">{{ texts().switchOnForVideo }}</p>\n } @else {\n <p class=\"message\">{{ texts().switchOnCameraHint }}</p>\n }\n }\n <div class=\"controls\">\n @if (isCameraSwitchShown()) {\n <!-- START CAMERA -->\n <ui-action-button\n class=\"toggleCameraButton\"\n [disabled]=\"device.isRecording()\"\n [descriptor]=\"toggleCameraButton()\"\n (actionClick)=\"device.toggleCamera()\"\n >\n </ui-action-button>\n }\n @if (device.keptFiles()) {\n <ui-action-button\n class=\"retakeButton\"\n [descriptor]=\"retakeButton()\"\n (actionClick)=\"device.retakeCapture()\"\n >\n </ui-action-button>\n <ui-action-button\n class=\"useButton\"\n [descriptor]=\"useButton()\"\n (actionClick)=\"device.useCapture()\"\n >\n </ui-action-button>\n } @else if (mode() === 'VIDEO') {\n <!-- RECORD / STOP VIDEO -->\n @if (device.isRecording()) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"device.stopRecording()\"\n >\n </ui-action-button>\n } @else {\n <ui-action-button\n class=\"startRecordingButton\"\n [disabled]=\"!device.isCameraActive()\"\n [descriptor]=\"startRecordingButton()\"\n (actionClick)=\"device.startRecording()\"\n >\n </ui-action-button>\n }\n } @else {\n <!-- CAPTURE CAMERA -->\n <ui-action-button\n class=\"capturePhotoButton\"\n [disabled]=\"!device.isCameraActive()\"\n [descriptor]=\"captureButton()\"\n (actionClick)=\"device.capturePhoto()\"\n >\n </ui-action-button>\n }\n @if (mode() === 'VIDEO' && !device.keptFiles()) {\n <!-- PICK VIDEO FILE -->\n <input\n #fileInput\n type=\"file\"\n accept=\"video/*\"\n (change)=\"onFileSelected($event)\"\n style=\"display: none\"\n />\n <ui-action-button\n class=\"pickVideoButton\"\n [disabled]=\"device.isRecording()\"\n [descriptor]=\"pickVideoButton()\"\n (actionClick)=\"fileInput.click()\"\n >\n </ui-action-button>\n }\n </div>\n </div>\n}\n", styles: [".photoWidgetContainer{border-radius:5px;width:100%;max-width:100%;display:flex;flex-direction:column;justify-content:center;border:1px solid #dee2e6}.message{text-align:center;color:#6b7280}video,canvas{border-top-right-radius:5px;border-top-left-radius:5px;width:100%}.controls{padding:.5rem;display:flex;gap:1rem;flex-direction:row;justify-content:center;background:#f8f9fa;border-top:1px solid #dee2e6;border-bottom-right-radius:5px;border-bottom-left-radius:5px}.controls ::ng-deep button{border-radius:3px}\n"], dependencies: [{ kind: "component", type: UiActionButtonComponent, selector: "ui-action-button", inputs: ["disabled", "descriptor", "code", "identifier", "viewActivated", "tab", "rowState", "addedCssClass", "addBasicClasses", "iconPlaceholder", "chords"], outputs: ["actionClick", "actionDoubleClick"] }] }); }
|
|
10313
|
+
}
|
|
10314
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: PhotoCaptureWidgetComponent, decorators: [{
|
|
10315
|
+
type: Component,
|
|
10316
|
+
args: [{ selector: 'photo-capture-widget', imports: [UiActionButtonComponent], template: "@if (isMobile()) {\n <input\n #fileInput\n type=\"file\"\n [accept]=\"mode() === 'VIDEO' ? 'video/*' : 'image/*'\"\n capture=\"environment\"\n (change)=\"onFileSelected($event)\"\n style=\"display: none\"\n />\n @if (!externalTrigger()) {\n <div class=\"photoWidgetContainer\">\n <div class=\"controls\">\n <ui-action-button [descriptor]=\"captureButton()\" (actionClick)=\"fileInput.click()\">\n </ui-action-button>\n </div>\n </div>\n }\n} @else if (!externalTrigger() || isPanelOpen()) {\n <div class=\"photoWidgetContainer\">\n <video #video autoplay [muted]=\"true\" [hidden]=\"device.isCaptured() || !device.isCameraActive()\"></video>\n <canvas #canvas [hidden]=\"!device.isCaptured() || device.isCameraActive()\"></canvas>\n @if (device.keptVideoUrl(); as videoUrl) {\n <video controls [src]=\"videoUrl\"></video>\n }\n @if (device.cameraError()) {\n <p class=\"message\">{{ texts().cameraError }}</p>\n } @else if (device.isRecording()) {\n <p class=\"message\">{{ texts().recording }}</p>\n } @else if (isSwitchOnHintShown()) {\n @if (mode() === 'VIDEO') {\n <p class=\"message\">{{ texts().switchOnForVideo }}</p>\n } @else {\n <p class=\"message\">{{ texts().switchOnCameraHint }}</p>\n }\n }\n <div class=\"controls\">\n @if (isCameraSwitchShown()) {\n <!-- START CAMERA -->\n <ui-action-button\n class=\"toggleCameraButton\"\n [disabled]=\"device.isRecording()\"\n [descriptor]=\"toggleCameraButton()\"\n (actionClick)=\"device.toggleCamera()\"\n >\n </ui-action-button>\n }\n @if (device.keptFiles()) {\n <ui-action-button\n class=\"retakeButton\"\n [descriptor]=\"retakeButton()\"\n (actionClick)=\"device.retakeCapture()\"\n >\n </ui-action-button>\n <ui-action-button\n class=\"useButton\"\n [descriptor]=\"useButton()\"\n (actionClick)=\"device.useCapture()\"\n >\n </ui-action-button>\n } @else if (mode() === 'VIDEO') {\n <!-- RECORD / STOP VIDEO -->\n @if (device.isRecording()) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"device.stopRecording()\"\n >\n </ui-action-button>\n } @else {\n <ui-action-button\n class=\"startRecordingButton\"\n [disabled]=\"!device.isCameraActive()\"\n [descriptor]=\"startRecordingButton()\"\n (actionClick)=\"device.startRecording()\"\n >\n </ui-action-button>\n }\n } @else {\n <!-- CAPTURE CAMERA -->\n <ui-action-button\n class=\"capturePhotoButton\"\n [disabled]=\"!device.isCameraActive()\"\n [descriptor]=\"captureButton()\"\n (actionClick)=\"device.capturePhoto()\"\n >\n </ui-action-button>\n }\n @if (mode() === 'VIDEO' && !device.keptFiles()) {\n <!-- PICK VIDEO FILE -->\n <input\n #fileInput\n type=\"file\"\n accept=\"video/*\"\n (change)=\"onFileSelected($event)\"\n style=\"display: none\"\n />\n <ui-action-button\n class=\"pickVideoButton\"\n [disabled]=\"device.isRecording()\"\n [descriptor]=\"pickVideoButton()\"\n (actionClick)=\"fileInput.click()\"\n >\n </ui-action-button>\n }\n </div>\n </div>\n}\n", styles: [".photoWidgetContainer{border-radius:5px;width:100%;max-width:100%;display:flex;flex-direction:column;justify-content:center;border:1px solid #dee2e6}.message{text-align:center;color:#6b7280}video,canvas{border-top-right-radius:5px;border-top-left-radius:5px;width:100%}.controls{padding:.5rem;display:flex;gap:1rem;flex-direction:row;justify-content:center;background:#f8f9fa;border-top:1px solid #dee2e6;border-bottom-right-radius:5px;border-bottom-left-radius:5px}.controls ::ng-deep button{border-radius:3px}\n"] }]
|
|
10317
|
+
}], propDecorators: { videoRef: [{
|
|
10318
|
+
type: ViewChild,
|
|
10319
|
+
args: ['video']
|
|
10320
|
+
}], canvasRef: [{
|
|
10321
|
+
type: ViewChild,
|
|
10322
|
+
args: ['canvas']
|
|
10323
|
+
}], fileInputRef: [{
|
|
10324
|
+
type: ViewChild,
|
|
10325
|
+
args: ['fileInput']
|
|
10326
|
+
}], maxFileSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFileSize", required: false }] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], externalTrigger: [{ type: i0.Input, args: [{ isSignal: true, alias: "externalTrigger", required: false }] }], texts: [{ type: i0.Input, args: [{ isSignal: true, alias: "texts", required: false }] }], deviceStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "deviceStart", required: false }] }], handover: [{ type: i0.Input, args: [{ isSignal: true, alias: "handover", required: false }] }], photoCaptured: [{ type: i0.Output, args: ["photoCaptured"] }] } });
|
|
10327
|
+
|
|
10328
|
+
/** The inline recorder strip of the upload zone and of the deprecated recordingUploader. */
|
|
10329
|
+
class VoiceRecordWidgetComponent {
|
|
10330
|
+
constructor() {
|
|
10331
|
+
this.openDirection = input('RIGHT', /* @ts-ignore */
|
|
10332
|
+
...(ngDevMode ? [{ debugName: "openDirection" }] : /* istanbul ignore next */ []));
|
|
10333
|
+
/** The zone's texts, the microphone error line's among them. */
|
|
10334
|
+
this.texts = input(UPLOAD_ZONE_DEFAULT_TEXTS, /* @ts-ignore */
|
|
10335
|
+
...(ngDevMode ? [{ debugName: "texts" }] : /* istanbul ignore next */ []));
|
|
10336
|
+
/** True in the upload zone only; the deprecated recordingUploader shows no error line. */
|
|
10337
|
+
this.showMicrophoneError = input(false, /* @ts-ignore */
|
|
10338
|
+
...(ngDevMode ? [{ debugName: "showMicrophoneError" }] : /* istanbul ignore next */ []));
|
|
10339
|
+
/** AUTO starts the recording when the widget opens, MANUAL at the microphone button. */
|
|
10340
|
+
this.deviceStart = input(CaptureDeviceStart.MANUAL, /* @ts-ignore */
|
|
10341
|
+
...(ngDevMode ? [{ debugName: "deviceStart" }] : /* istanbul ignore next */ []));
|
|
10342
|
+
/** CONFIRM keeps a stopped recording for playback and save, IMMEDIATE hands it over. */
|
|
10343
|
+
this.handover = input(CaptureHandover.CONFIRM, /* @ts-ignore */
|
|
10344
|
+
...(ngDevMode ? [{ debugName: "handover" }] : /* istanbul ignore next */ []));
|
|
10345
|
+
/** True under the zone's Capture Settings; everywhere else the save stays an icon. */
|
|
10346
|
+
this.showSaveTitle = input(false, /* @ts-ignore */
|
|
10347
|
+
...(ngDevMode ? [{ debugName: "showSaveTitle" }] : /* istanbul ignore next */ []));
|
|
10348
|
+
this.recordingSaved = output();
|
|
10349
|
+
this.device = new MicrophoneDevice({
|
|
10350
|
+
handover: this.handover,
|
|
10351
|
+
ngZone: inject(NgZone),
|
|
10352
|
+
getWaveformElement: () => this.waveformRef?.nativeElement,
|
|
10353
|
+
getButtonElement: () => this.mainButton.nativeElement,
|
|
10354
|
+
getThemeElement: () => document.documentElement,
|
|
10355
|
+
handOver: (file) => this.recordingSaved.emit(file),
|
|
10356
|
+
});
|
|
10357
|
+
this.micButton = {
|
|
10358
|
+
title: '',
|
|
10359
|
+
type: UiActionButtonType.ICON,
|
|
10360
|
+
icon: 'microphone',
|
|
10361
|
+
iconPosition: IconPosition.PRE,
|
|
10362
|
+
color: 'primary',
|
|
10363
|
+
};
|
|
10364
|
+
this.stopButton = {
|
|
10365
|
+
title: '',
|
|
10366
|
+
type: UiActionButtonType.ICON,
|
|
10367
|
+
icon: 'stop-circle',
|
|
10368
|
+
iconPosition: IconPosition.PRE,
|
|
10369
|
+
iconColor: 'red',
|
|
10370
|
+
color: 'primary',
|
|
10371
|
+
};
|
|
10372
|
+
this.saveButton = {
|
|
10373
|
+
title: '',
|
|
10374
|
+
type: UiActionButtonType.ICON,
|
|
10375
|
+
icon: 'save',
|
|
10376
|
+
iconPosition: IconPosition.PRE,
|
|
10377
|
+
color: 'primary',
|
|
10378
|
+
};
|
|
10379
|
+
this.effectiveSaveButton = computed(() => this.showSaveTitle()
|
|
10380
|
+
? { ...this.saveButton, title: this.texts().save, type: UiActionButtonType.RAISED }
|
|
10381
|
+
: this.saveButton, /* @ts-ignore */
|
|
10382
|
+
...(ngDevMode ? [{ debugName: "effectiveSaveButton" }] : /* istanbul ignore next */ []));
|
|
10383
|
+
}
|
|
10384
|
+
ngOnInit() {
|
|
10385
|
+
this.playButton = {
|
|
10386
|
+
title: '',
|
|
10387
|
+
type: UiActionButtonType.ICON,
|
|
10388
|
+
icon: 'play_circle',
|
|
10389
|
+
iconPosition: IconPosition.PRE,
|
|
10390
|
+
color: 'primary',
|
|
10391
|
+
};
|
|
10392
|
+
this.stopPlaybackButton = {
|
|
10393
|
+
title: '',
|
|
10394
|
+
type: UiActionButtonType.ICON,
|
|
10395
|
+
icon: 'stop_circle',
|
|
10396
|
+
iconPosition: IconPosition.PRE,
|
|
10397
|
+
color: 'red',
|
|
10398
|
+
};
|
|
10399
|
+
}
|
|
10400
|
+
ngAfterViewInit() {
|
|
10401
|
+
if (this.deviceStart() === CaptureDeviceStart.AUTO) {
|
|
10402
|
+
this.device.requestRecording();
|
|
10403
|
+
}
|
|
10404
|
+
}
|
|
10405
|
+
get isLeftDirection() {
|
|
10406
|
+
return this.openDirection() === 'LEFT';
|
|
10407
|
+
}
|
|
10408
|
+
ngOnDestroy() {
|
|
10409
|
+
this.device.release();
|
|
10410
|
+
}
|
|
10411
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VoiceRecordWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10412
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: VoiceRecordWidgetComponent, isStandalone: true, selector: "voice-record-widget", inputs: { openDirection: { classPropertyName: "openDirection", publicName: "openDirection", isSignal: true, isRequired: false, transformFunction: null }, texts: { classPropertyName: "texts", publicName: "texts", isSignal: true, isRequired: false, transformFunction: null }, showMicrophoneError: { classPropertyName: "showMicrophoneError", publicName: "showMicrophoneError", isSignal: true, isRequired: false, transformFunction: null }, deviceStart: { classPropertyName: "deviceStart", publicName: "deviceStart", isSignal: true, isRequired: false, transformFunction: null }, handover: { classPropertyName: "handover", publicName: "handover", isSignal: true, isRequired: false, transformFunction: null }, showSaveTitle: { classPropertyName: "showSaveTitle", publicName: "showSaveTitle", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { recordingSaved: "recordingSaved" }, viewQueries: [{ propertyName: "waveformRef", first: true, predicate: ["waveform"], descendants: true }, { propertyName: "mainButton", first: true, predicate: ["mainButton"], descendants: true, read: ElementRef }], ngImport: i0, template: "<div\n class=\"recorderContainer\"\n [class.direction-left]=\"isLeftDirection\"\n [class.direction-right]=\"!isLeftDirection\"\n [class.active]=\"device.isWaveformShown()\"\n [class.isRecording]=\"device.isRecording()\"\n [class.hasRecording]=\"device.recordedBlob()\"\n >\n <div class=\"recorderMain\">\n <div class=\"recorderToolbar\">\n <!-- Start Recording -->\n @if (!device.isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"micButton\"\n (actionClick)=\"device.requestRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Stop Recording -->\n @if (device.isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"stopButton\"\n (actionClick)=\"device.stopRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Save Recording -->\n @if (device.recordedBlob()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"effectiveSaveButton()\"\n (actionClick)=\"device.saveRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Start Play -->\n @if (device.recordedBlob() && !device.isPlaying()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"playButton\"\n (actionClick)=\"device.replayRecording()\"\n ></ui-action-button>\n }\n\n <!-- Stop Play -->\n @if (device.recordedBlob() && device.isPlaying()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"stopPlaybackButton\"\n (actionClick)=\"device.replayRecording()\"\n ></ui-action-button>\n }\n\n @if (device.isRecording()) {\n <div class=\"progress\">{{ device.time() }}</div>\n }\n </div>\n\n <div class=\"waveformBand\" [class.open]=\"device.isWaveformShown()\">\n <div class=\"waveformGrid\"></div>\n <div class=\"waveform\" #waveform></div>\n </div>\n </div>\n</div>\n@if (showMicrophoneError() && device.microphoneError()) {\n <p class=\"message\">{{ texts().microphoneError }}</p>\n}\n", styles: [":host{display:block;width:100%;min-width:0}.message{text-align:center;color:#6b7280}:host ::ng-deep .waveform{height:unset!important}.recorderContainer{display:flex;flex-direction:row;justify-content:space-between;width:100%;min-width:0;align-items:center;gap:.5rem}.active ::ng-deep .recorderToolbar{border:1px solid #e5e7eb;border-radius:.5rem}.isRecording ::ng-deep .recorderToolbar{padding-right:1rem}.recorderToolbar ::ng-deep .sb4-red,.recorderToolbar ::ng-deep .sb4-primary{border:1px solid #e5e7eb;box-shadow:unset}.active ::ng-deep .recorderToolbar ::ng-deep .sb4-red,.active ::ng-deep .recorderToolbar ::ng-deep .sb4-primary{border:unset}.mainButton ::ng-deep .sb4-red,.mainButton ::ng-deep .sb4-primary{padding:.5rem .75rem}.recorderMain{display:flex;align-items:center;min-width:0;flex:1}.direction-right .recorderMain{flex-direction:row}.direction-left .recorderMain{flex-direction:row-reverse}.waveform{width:100%;height:45px}.waveformBand{position:relative;display:grid;align-items:center;width:0;max-width:0;min-width:0;opacity:0;overflow:hidden;border-radius:5px;flex:1 1 0;transform:scaleX(.96);will-change:opacity,transform,margin;transition:max-width .22s ease,opacity .18s ease,margin .22s ease,transform .22s ease}.waveformBand.open{min-width:35px;max-width:100%;opacity:1;transform:scaleX(1)}.direction-right .waveformBand{margin-left:0}.direction-left .waveformBand{margin-right:0}.direction-right .waveformBand.open{margin-left:.45rem}.direction-left .waveformBand.open{margin-right:.45rem}.waveformGrid{position:absolute;inset:0;border:1px solid #e5e7eb;border-radius:.5rem;background:repeating-linear-gradient(to right,rgba(148,163,184,.35) 0,rgba(148,163,184,.35) 2px,transparent 2px,transparent 8px),linear-gradient(to bottom,#94a3b814,#94a3b805);pointer-events:none}.recorderToolbar{background-color:transparent;display:flex;flex-direction:row;align-items:center}.progress{align-content:center;font-size:1rem;height:100%;color:#6b7280}.hasRecording ::ng-deep .recorderToolbar ui-action-button{border-right:1px solid #e5e7eb}.hasRecording ::ng-deep .recorderToolbar ui-action-button:last-child{border-right:unset}\n"], dependencies: [{ kind: "component", type: UiActionButtonComponent, selector: "ui-action-button", inputs: ["disabled", "descriptor", "code", "identifier", "viewActivated", "tab", "rowState", "addedCssClass", "addBasicClasses", "iconPlaceholder", "chords"], outputs: ["actionClick", "actionDoubleClick"] }] }); }
|
|
10413
|
+
}
|
|
10414
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VoiceRecordWidgetComponent, decorators: [{
|
|
10415
|
+
type: Component,
|
|
10416
|
+
args: [{ selector: 'voice-record-widget', imports: [UiActionButtonComponent], template: "<div\n class=\"recorderContainer\"\n [class.direction-left]=\"isLeftDirection\"\n [class.direction-right]=\"!isLeftDirection\"\n [class.active]=\"device.isWaveformShown()\"\n [class.isRecording]=\"device.isRecording()\"\n [class.hasRecording]=\"device.recordedBlob()\"\n >\n <div class=\"recorderMain\">\n <div class=\"recorderToolbar\">\n <!-- Start Recording -->\n @if (!device.isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"micButton\"\n (actionClick)=\"device.requestRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Stop Recording -->\n @if (device.isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"stopButton\"\n (actionClick)=\"device.stopRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Save Recording -->\n @if (device.recordedBlob()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"effectiveSaveButton()\"\n (actionClick)=\"device.saveRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Start Play -->\n @if (device.recordedBlob() && !device.isPlaying()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"playButton\"\n (actionClick)=\"device.replayRecording()\"\n ></ui-action-button>\n }\n\n <!-- Stop Play -->\n @if (device.recordedBlob() && device.isPlaying()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"stopPlaybackButton\"\n (actionClick)=\"device.replayRecording()\"\n ></ui-action-button>\n }\n\n @if (device.isRecording()) {\n <div class=\"progress\">{{ device.time() }}</div>\n }\n </div>\n\n <div class=\"waveformBand\" [class.open]=\"device.isWaveformShown()\">\n <div class=\"waveformGrid\"></div>\n <div class=\"waveform\" #waveform></div>\n </div>\n </div>\n</div>\n@if (showMicrophoneError() && device.microphoneError()) {\n <p class=\"message\">{{ texts().microphoneError }}</p>\n}\n", styles: [":host{display:block;width:100%;min-width:0}.message{text-align:center;color:#6b7280}:host ::ng-deep .waveform{height:unset!important}.recorderContainer{display:flex;flex-direction:row;justify-content:space-between;width:100%;min-width:0;align-items:center;gap:.5rem}.active ::ng-deep .recorderToolbar{border:1px solid #e5e7eb;border-radius:.5rem}.isRecording ::ng-deep .recorderToolbar{padding-right:1rem}.recorderToolbar ::ng-deep .sb4-red,.recorderToolbar ::ng-deep .sb4-primary{border:1px solid #e5e7eb;box-shadow:unset}.active ::ng-deep .recorderToolbar ::ng-deep .sb4-red,.active ::ng-deep .recorderToolbar ::ng-deep .sb4-primary{border:unset}.mainButton ::ng-deep .sb4-red,.mainButton ::ng-deep .sb4-primary{padding:.5rem .75rem}.recorderMain{display:flex;align-items:center;min-width:0;flex:1}.direction-right .recorderMain{flex-direction:row}.direction-left .recorderMain{flex-direction:row-reverse}.waveform{width:100%;height:45px}.waveformBand{position:relative;display:grid;align-items:center;width:0;max-width:0;min-width:0;opacity:0;overflow:hidden;border-radius:5px;flex:1 1 0;transform:scaleX(.96);will-change:opacity,transform,margin;transition:max-width .22s ease,opacity .18s ease,margin .22s ease,transform .22s ease}.waveformBand.open{min-width:35px;max-width:100%;opacity:1;transform:scaleX(1)}.direction-right .waveformBand{margin-left:0}.direction-left .waveformBand{margin-right:0}.direction-right .waveformBand.open{margin-left:.45rem}.direction-left .waveformBand.open{margin-right:.45rem}.waveformGrid{position:absolute;inset:0;border:1px solid #e5e7eb;border-radius:.5rem;background:repeating-linear-gradient(to right,rgba(148,163,184,.35) 0,rgba(148,163,184,.35) 2px,transparent 2px,transparent 8px),linear-gradient(to bottom,#94a3b814,#94a3b805);pointer-events:none}.recorderToolbar{background-color:transparent;display:flex;flex-direction:row;align-items:center}.progress{align-content:center;font-size:1rem;height:100%;color:#6b7280}.hasRecording ::ng-deep .recorderToolbar ui-action-button{border-right:1px solid #e5e7eb}.hasRecording ::ng-deep .recorderToolbar ui-action-button:last-child{border-right:unset}\n"] }]
|
|
10417
|
+
}], propDecorators: { waveformRef: [{
|
|
10418
|
+
type: ViewChild,
|
|
10419
|
+
args: ['waveform']
|
|
10420
|
+
}], mainButton: [{
|
|
10421
|
+
type: ViewChild,
|
|
10422
|
+
args: ['mainButton', { read: ElementRef }]
|
|
10423
|
+
}], openDirection: [{ type: i0.Input, args: [{ isSignal: true, alias: "openDirection", required: false }] }], texts: [{ type: i0.Input, args: [{ isSignal: true, alias: "texts", required: false }] }], showMicrophoneError: [{ type: i0.Input, args: [{ isSignal: true, alias: "showMicrophoneError", required: false }] }], deviceStart: [{ type: i0.Input, args: [{ isSignal: true, alias: "deviceStart", required: false }] }], handover: [{ type: i0.Input, args: [{ isSignal: true, alias: "handover", required: false }] }], showSaveTitle: [{ type: i0.Input, args: [{ isSignal: true, alias: "showSaveTitle", required: false }] }], recordingSaved: [{ type: i0.Output, args: ["recordingSaved"] }] } });
|
|
10424
|
+
|
|
10297
10425
|
class UploadWidgetComponent {
|
|
10298
10426
|
constructor() {
|
|
10299
10427
|
this.captureDialogService = inject(CaptureDialogService);
|
|
@@ -10475,7 +10603,7 @@ class UploadWidgetComponent {
|
|
|
10475
10603
|
};
|
|
10476
10604
|
}
|
|
10477
10605
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: UploadWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
10478
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: UploadWidgetComponent, isStandalone: true, selector: "smart-upload-widget", inputs: { uploadDescriptor: { classPropertyName: "uploadDescriptor", publicName: "uploadDescriptor", isSignal: true, isRequired: false, transformFunction: null }, isMultiple: { classPropertyName: "isMultiple", publicName: "isMultiple", isSignal: true, isRequired: true, transformFunction: null }, isDisabled: { classPropertyName: "isDisabled", publicName: "isDisabled", isSignal: true, isRequired: false, transformFunction: null }, sources: { classPropertyName: "sources", publicName: "sources", isSignal: true, isRequired: false, transformFunction: null }, capture: { classPropertyName: "capture", publicName: "capture", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { uploadFilesEvent: "uploadFilesEvent" }, viewQueries: [{ propertyName: "fileUploadMaterial", first: true, predicate: ["fileUploadMaterial"], descendants: true }, { propertyName: "photoCapture", first: true, predicate: ["photoCapture"], descendants: true }, { propertyName: "videoCapture", first: true, predicate: ["videoCapture"], descendants: true }], ngImport: i0, template: "<div class=\"sb4UploadWidget\">\n @if (hasSources()) {\n <!-- CAPTURE ROW OF THE SOURCES, hidden in view mode -->\n @if (getCaptureSources().length && !isDisabled()) {\n <div class=\"captureRow\" [class.captureOnly]=\"isCaptureOnly()\">\n <div class=\"captureButtons\">\n @for (source of getCaptureSources(); track source) {\n <ui-action-button\n class=\"{{ getCaptureButtonClass(source) }}\"\n [descriptor]=\"getCaptureButton(source)\"\n (actionClick)=\"startCapture(source)\"\n ></ui-action-button>\n }\n </div>\n @if (isCaptureOnly()) {\n <div class=\"captureHint\">{{ getCaptureOnlyHint() }}</div>\n }\n </div>\n }\n @if (isSourceNeeded(fileUploaderSource.CAMERA_PHOTO)) {\n <photo-capture-widget\n #photoCapture\n [mode]=\"'PHOTO'\"\n [externalTrigger]=\"true\"\n [deviceStart]=\"captureSettings().deviceStart\"\n [handover]=\"captureSettings().handover\"\n [texts]=\"texts\"\n [maxFileSize]=\"getMaxFileSize()\"\n (photoCaptured)=\"uploadImage($event)\"\n ></photo-capture-widget>\n }\n @if (isSourceNeeded(fileUploaderSource.CAMERA_VIDEO)) {\n <photo-capture-widget\n #videoCapture\n [mode]=\"'VIDEO'\"\n [externalTrigger]=\"true\"\n [deviceStart]=\"captureSettings().deviceStart\"\n [handover]=\"captureSettings().handover\"\n [texts]=\"texts\"\n (photoCaptured)=\"uploadImage($event)\"\n ></photo-capture-widget>\n }\n @if (isRecorderOpen()) {\n <voice-record-widget\n [openDirection]=\"captureSettings().openDirection\"\n [deviceStart]=\"captureSettings().deviceStart\"\n [handover]=\"captureSettings().handover\"\n [showSaveTitle]=\"true\"\n [texts]=\"texts\"\n [showMicrophoneError]=\"true\"\n (recordingSaved)=\"uploadRecording($event)\"\n ></voice-record-widget>\n }\n } @else {\n <!-- IMAGE TYPE WIDGET -->\n @if (widgetNeeded(uploadWidgetType.IMAGE) || widgetNeeded(uploadWidgetType.ALL)) {\n <photo-capture-widget\n [maxFileSize]=\"getMaxFileSize()\"\n [texts]=\"texts\"\n (photoCaptured)=\"uploadImage($event)\"\n >\n </photo-capture-widget>\n }\n\n <!-- SOUND TYPE WIDGET -->\n @if (widgetNeeded(uploadWidgetType.SOUND) || widgetNeeded(uploadWidgetType.ALL)) {\n <voice-record-widget\n [texts]=\"texts\"\n [showMicrophoneError]=\"true\"\n (recordingSaved)=\"uploadRecording($event)\"\n ></voice-record-widget>\n }\n }\n <smartfileuploader\n #fileUploadMaterial\n [i18n]=\"i18n\"\n [fileFormats]=\"fileFormats\"\n [maxSizeMb]=\"maxSizeMb\"\n [uploadCallback]=\"upload.bind(this)\"\n [isMultiple]=\"isMultiple()\"\n [autoUpload]=\"isUploadImmediate()\"\n [isDisabled]=\"isDisabled()\"\n [pickerShown]=\"isPickerShown()\"\n ></smartfileuploader>\n</div>\n", styles: ["photo-capture-widget{width:100%;max-width:100%;overflow:hidden;display:flex;flex-direction:column;align-items:center}voice-record-widget{box-sizing:border-box;padding:.5rem;border-bottom:1px solid #e5e7eb}voice-record-widget ::ng-deep .sb4-red,voice-record-widget ::ng-deep .sb4-primary,voice-record-widget ::ng-deep ui-action-button{border:unset!important}@media(max-width:900px){:host{width:100%}}.captureRow{display:flex;justify-content:center;padding:.5rem;border-bottom:1px solid #e5e7eb}.captureRow.captureOnly{border-bottom:unset;flex-direction:column;align-items:center;padding:.875rem .5rem;gap:.375rem}.captureButtons{display:flex;gap:.5rem}.captureButtons ::ng-deep button{border:unset}.captureHint{font-size:12px;color:#6b7280}\n"], dependencies: [{ kind: "component", type: PhotoCaptureWidgetComponent, selector: "photo-capture-widget", inputs: ["maxFileSize", "mode", "externalTrigger", "texts", "deviceStart", "handover"
|
|
10606
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: UploadWidgetComponent, isStandalone: true, selector: "smart-upload-widget", inputs: { uploadDescriptor: { classPropertyName: "uploadDescriptor", publicName: "uploadDescriptor", isSignal: true, isRequired: false, transformFunction: null }, isMultiple: { classPropertyName: "isMultiple", publicName: "isMultiple", isSignal: true, isRequired: true, transformFunction: null }, isDisabled: { classPropertyName: "isDisabled", publicName: "isDisabled", isSignal: true, isRequired: false, transformFunction: null }, sources: { classPropertyName: "sources", publicName: "sources", isSignal: true, isRequired: false, transformFunction: null }, capture: { classPropertyName: "capture", publicName: "capture", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { uploadFilesEvent: "uploadFilesEvent" }, viewQueries: [{ propertyName: "fileUploadMaterial", first: true, predicate: ["fileUploadMaterial"], descendants: true }, { propertyName: "photoCapture", first: true, predicate: ["photoCapture"], descendants: true }, { propertyName: "videoCapture", first: true, predicate: ["videoCapture"], descendants: true }], ngImport: i0, template: "<div class=\"sb4UploadWidget\">\n @if (hasSources()) {\n <!-- CAPTURE ROW OF THE SOURCES, hidden in view mode -->\n @if (getCaptureSources().length && !isDisabled()) {\n <div class=\"captureRow\" [class.captureOnly]=\"isCaptureOnly()\">\n <div class=\"captureButtons\">\n @for (source of getCaptureSources(); track source) {\n <ui-action-button\n class=\"{{ getCaptureButtonClass(source) }}\"\n [descriptor]=\"getCaptureButton(source)\"\n (actionClick)=\"startCapture(source)\"\n ></ui-action-button>\n }\n </div>\n @if (isCaptureOnly()) {\n <div class=\"captureHint\">{{ getCaptureOnlyHint() }}</div>\n }\n </div>\n }\n @if (isSourceNeeded(fileUploaderSource.CAMERA_PHOTO)) {\n <photo-capture-widget\n #photoCapture\n [mode]=\"'PHOTO'\"\n [externalTrigger]=\"true\"\n [deviceStart]=\"captureSettings().deviceStart\"\n [handover]=\"captureSettings().handover\"\n [texts]=\"texts\"\n [maxFileSize]=\"getMaxFileSize()\"\n (photoCaptured)=\"uploadImage($event)\"\n ></photo-capture-widget>\n }\n @if (isSourceNeeded(fileUploaderSource.CAMERA_VIDEO)) {\n <photo-capture-widget\n #videoCapture\n [mode]=\"'VIDEO'\"\n [externalTrigger]=\"true\"\n [deviceStart]=\"captureSettings().deviceStart\"\n [handover]=\"captureSettings().handover\"\n [texts]=\"texts\"\n (photoCaptured)=\"uploadImage($event)\"\n ></photo-capture-widget>\n }\n @if (isRecorderOpen()) {\n <voice-record-widget\n [openDirection]=\"captureSettings().openDirection\"\n [deviceStart]=\"captureSettings().deviceStart\"\n [handover]=\"captureSettings().handover\"\n [showSaveTitle]=\"true\"\n [texts]=\"texts\"\n [showMicrophoneError]=\"true\"\n (recordingSaved)=\"uploadRecording($event)\"\n ></voice-record-widget>\n }\n } @else {\n <!-- IMAGE TYPE WIDGET -->\n @if (widgetNeeded(uploadWidgetType.IMAGE) || widgetNeeded(uploadWidgetType.ALL)) {\n <photo-capture-widget\n [maxFileSize]=\"getMaxFileSize()\"\n [texts]=\"texts\"\n (photoCaptured)=\"uploadImage($event)\"\n >\n </photo-capture-widget>\n }\n\n <!-- SOUND TYPE WIDGET -->\n @if (widgetNeeded(uploadWidgetType.SOUND) || widgetNeeded(uploadWidgetType.ALL)) {\n <voice-record-widget\n [texts]=\"texts\"\n [showMicrophoneError]=\"true\"\n (recordingSaved)=\"uploadRecording($event)\"\n ></voice-record-widget>\n }\n }\n <smartfileuploader\n #fileUploadMaterial\n [i18n]=\"i18n\"\n [fileFormats]=\"fileFormats\"\n [maxSizeMb]=\"maxSizeMb\"\n [uploadCallback]=\"upload.bind(this)\"\n [isMultiple]=\"isMultiple()\"\n [autoUpload]=\"isUploadImmediate()\"\n [isDisabled]=\"isDisabled()\"\n [pickerShown]=\"isPickerShown()\"\n ></smartfileuploader>\n</div>\n", styles: ["photo-capture-widget{width:100%;max-width:100%;overflow:hidden;display:flex;flex-direction:column;align-items:center}voice-record-widget{box-sizing:border-box;padding:.5rem;border-bottom:1px solid #e5e7eb}voice-record-widget ::ng-deep .sb4-red,voice-record-widget ::ng-deep .sb4-primary,voice-record-widget ::ng-deep ui-action-button{border:unset!important}@media(max-width:900px){:host{width:100%}}.captureRow{display:flex;justify-content:center;padding:.5rem;border-bottom:1px solid #e5e7eb}.captureRow.captureOnly{border-bottom:unset;flex-direction:column;align-items:center;padding:.875rem .5rem;gap:.375rem}.captureButtons{display:flex;gap:.5rem}.captureButtons ::ng-deep button{border:unset}.captureHint{font-size:12px;color:#6b7280}\n"], dependencies: [{ kind: "component", type: PhotoCaptureWidgetComponent, selector: "photo-capture-widget", inputs: ["maxFileSize", "mode", "externalTrigger", "texts", "deviceStart", "handover"], outputs: ["photoCaptured"] }, { kind: "component", type: VoiceRecordWidgetComponent, selector: "voice-record-widget", inputs: ["openDirection", "texts", "showMicrophoneError", "deviceStart", "handover", "showSaveTitle"], outputs: ["recordingSaved"] }, { kind: "component", type: SmartfileuploaderComponent, selector: "smartfileuploader", inputs: ["uploadCallback", "fileFormats", "maxSizeMb", "i18n", "useIconButton", "isMultiple", "autoUpload", "isDisabled", "pickerShown"] }, { kind: "component", type: UiActionButtonComponent, selector: "ui-action-button", inputs: ["disabled", "descriptor", "code", "identifier", "viewActivated", "tab", "rowState", "addedCssClass", "addBasicClasses", "iconPlaceholder", "chords"], outputs: ["actionClick", "actionDoubleClick"] }] }); }
|
|
10479
10607
|
}
|
|
10480
10608
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: UploadWidgetComponent, decorators: [{
|
|
10481
10609
|
type: Component,
|
|
@@ -12163,7 +12291,7 @@ class SmartVoiceRecorderComponent {
|
|
|
12163
12291
|
useExisting: forwardRef(() => SmartVoiceRecorderComponent),
|
|
12164
12292
|
multi: true,
|
|
12165
12293
|
},
|
|
12166
|
-
], ngImport: i0, template: "<voice-record-widget [openDirection]=\"openDirection\" (recordingSaved)=\"uploadRecording($event)\">\n</voice-record-widget>\n", styles: [":host{display:block;width:100%;min-width:0}\n"], dependencies: [{ kind: "component", type: VoiceRecordWidgetComponent, selector: "voice-record-widget", inputs: ["openDirection", "texts", "showMicrophoneError", "deviceStart", "handover", "showSaveTitle"
|
|
12294
|
+
], ngImport: i0, template: "<voice-record-widget [openDirection]=\"openDirection\" (recordingSaved)=\"uploadRecording($event)\">\n</voice-record-widget>\n", styles: [":host{display:block;width:100%;min-width:0}\n"], dependencies: [{ kind: "component", type: VoiceRecordWidgetComponent, selector: "voice-record-widget", inputs: ["openDirection", "texts", "showMicrophoneError", "deviceStart", "handover", "showSaveTitle"], outputs: ["recordingSaved"] }] }); }
|
|
12167
12295
|
}
|
|
12168
12296
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: SmartVoiceRecorderComponent, decorators: [{
|
|
12169
12297
|
type: Component,
|
|
@@ -17242,6 +17370,10 @@ class Table {
|
|
|
17242
17370
|
}
|
|
17243
17371
|
}
|
|
17244
17372
|
async onToggle(element, event) {
|
|
17373
|
+
// The mobile table has no material table, so no detail row to open.
|
|
17374
|
+
if (!this.myTableChild) {
|
|
17375
|
+
return;
|
|
17376
|
+
}
|
|
17245
17377
|
if (!this.smartTable.rowExpander) {
|
|
17246
17378
|
throw new Error('SmartTableRowExpander not found!. Please provide a SmartTableRowExpander implementation.');
|
|
17247
17379
|
}
|