@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 with a decimal comma, as the Capture Dialog's captions show it. */
9189
- static formatCapturedSize(bytes) {
9190
- return UploadWidgetUtils.formatSize(bytes).replace('.', ',');
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
- class PhotoCaptureWidgetComponent {
9460
- constructor() {
9461
- this.injector = inject(Injector);
9462
- /** The take, record, retake and use buttons, for a host that draws them itself. */
9463
- this.actionButtonsTemplate = viewChild('actionButtons', /* @ts-ignore */
9464
- ...(ngDevMode ? [{ debugName: "actionButtonsTemplate" }] : /* istanbul ignore next */ []));
9465
- this.maxFileSize = input(/* @ts-ignore */
9466
- ...(ngDevMode ? [undefined, { debugName: "maxFileSize" }] : /* istanbul ignore next */ []));
9467
- /** PHOTO takes a picture; VIDEO records a video with the webcam or picks one on a desktop. */
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: AUTO then shows neither the switch nor its hint. */
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.isRecordingState = signal(false, /* @ts-ignore */
9504
- ...(ngDevMode ? [{ debugName: "isRecordingState" }] : /* istanbul ignore next */ []));
9505
- this.isCapturedState = signal(false, /* @ts-ignore */
9506
- ...(ngDevMode ? [{ debugName: "isCapturedState" }] : /* istanbul ignore next */ []));
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() || this.cameraActive || this.isRecording || !!this.keptFiles(), /* @ts-ignore */
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
- this.videoRef.nativeElement.srcObject = this.stream;
9642
- this.videoRef.nativeElement.play();
9602
+ const video = this.options.getVideoElement();
9603
+ video.srcObject = this.stream;
9604
+ video.play();
9643
9605
  this.isCameraStarting.set(false);
9644
- this.cameraActive = true;
9645
- this.isCaptured = false;
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 = false;
9631
+ this.isRecording.set(false);
9670
9632
  this.stream?.getTracks().forEach((track) => track.stop());
9671
9633
  this.stream = undefined;
9672
- this.cameraActive = false;
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.videoRef.nativeElement;
9676
- const canvas = this.canvasRef.nativeElement;
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 this.compressImageFile(file, maxFileSize);
9659
+ file = await UploadWidgetUtils.compressImageFile(file, maxFileSize);
9690
9660
  }
9691
9661
  this.handOverCapture([file]);
9692
- this.isCaptured = true;
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 = true;
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.photoCaptured.emit(files);
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.photoCaptured.emit(this.keptFiles());
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 = false;
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
- class VoiceRecordWidgetComponent {
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.ngZone = inject(NgZone);
9877
- this.changeDetectorRef = inject(ChangeDetectorRef);
9878
- /** The stop and save buttons, for a host that draws them itself. */
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.openDirection = input('RIGHT', /* @ts-ignore */
9882
- ...(ngDevMode ? [{ debugName: "openDirection" }] : /* istanbul ignore next */ []));
9883
- /** The zone's texts, the microphone error line's among them. */
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
- /** CONFIRM keeps a stopped recording for playback and save, IMMEDIATE hands it over. */
9893
- this.handover = input(CaptureHandover.CONFIRM, /* @ts-ignore */
9752
+ this.handover = input.required(/* @ts-ignore */
9894
9753
  ...(ngDevMode ? [{ debugName: "handover" }] : /* istanbul ignore next */ []));
9895
- /** True under the zone's Capture Settings; everywhere else the save stays an icon. */
9896
- this.showSaveTitle = input(false, /* @ts-ignore */
9897
- ...(ngDevMode ? [{ debugName: "showSaveTitle" }] : /* istanbul ignore next */ []));
9898
- /** True in a host that draws actionButtonsTemplate: the strip keeps no save of its own. */
9899
- this.actionsInHost = input(false, /* @ts-ignore */
9900
- ...(ngDevMode ? [{ debugName: "actionsInHost" }] : /* istanbul ignore next */ []));
9901
- this.recordingSaved = output();
9902
- /** Signals, as the host reads them through isCaptureUnderWay and actionButtonsTemplate. */
9903
- this.isRecording = signal(false, /* @ts-ignore */
9904
- ...(ngDevMode ? [{ debugName: "isRecording" }] : /* istanbul ignore next */ []));
9905
- this.recordedBlob = signal(null, /* @ts-ignore */
9906
- ...(ngDevMode ? [{ debugName: "recordedBlob" }] : /* istanbul ignore next */ []));
9907
- /** True while a host may not lose the capture by accident: the microphone is asked for, the
9908
- * recording runs or waits for the save. */
9909
- this.isCaptureUnderWay = computed(() => this.isRecording() || !!this.recordedBlob(), /* @ts-ignore */
9910
- ...(ngDevMode ? [{ debugName: "isCaptureUnderWay" }] : /* istanbul ignore next */ []));
9911
- this.time = '00:00';
9912
- this.isPlaying = false;
9913
- this.isWaveformOpen = false;
9914
- /** Set when the browser gives no microphone, until the next start. */
9915
- this.microphoneError = signal(false, /* @ts-ignore */
9916
- ...(ngDevMode ? [{ debugName: "microphoneError" }] : /* istanbul ignore next */ []));
9917
- this.micButton = {
9918
- title: '',
9919
- type: UiActionButtonType.ICON,
9920
- icon: 'microphone',
9921
- iconPosition: IconPosition.PRE,
9922
- color: 'primary',
9923
- };
9924
- this.stopButton = {
9925
- title: '',
9926
- type: UiActionButtonType.ICON,
9927
- icon: 'stop-circle',
9928
- iconPosition: IconPosition.PRE,
9929
- iconColor: 'red',
9930
- color: 'primary',
9931
- };
9932
- /** Only a theme colour's name paints the icon: the deprecated recorder keeps its look. */
9933
- this.effectiveStopButton = computed(() => this.actionsInHost() ? { ...this.stopButton, color: 'warn', iconColor: 'warn' } : this.stopButton, /* @ts-ignore */
9934
- ...(ngDevMode ? [{ debugName: "effectiveStopButton" }] : /* istanbul ignore next */ []));
9935
- this.saveButton = {
9936
- title: '',
9937
- type: UiActionButtonType.ICON,
9938
- icon: 'save',
9939
- iconPosition: IconPosition.PRE,
9940
- color: 'primary',
9941
- };
9942
- this.effectiveSaveButton = computed(() => this.showSaveTitle()
9943
- ? { ...this.saveButton, title: this.texts().save, type: UiActionButtonType.RAISED }
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.requestRecording();
9806
+ afterNextRender(() => this.device.startCamera(), { injector: this.injector });
9974
9807
  }
9975
9808
  }
9976
- get isLeftDirection() {
9977
- return this.openDirection() === 'LEFT';
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
- if (this.wavesurfer || !this.waveformRef?.nativeElement) {
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
- this.waveformRef.nativeElement.style.padding = '0.1rem';
9988
- this.initWaveSurfer();
9855
+ waveform.style.padding = '0.1rem';
9856
+ this.initWaveSurfer(waveform);
9989
9857
  }
9990
- initWaveSurfer() {
9991
- let body = document.querySelector('body');
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
- let el = this.mainButton.nativeElement.querySelector('.sb4-primary');
9997
- let remInPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
9998
- let height = (el?.getBoundingClientRect().height ?? 0) - remInPx * 0.1;
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: this.waveformRef.nativeElement,
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.updateViewState(() => {
9885
+ this.options.ngZone.run(() => {
10020
9886
  this.wavesurfer?.play();
10021
- this.isPlaying = true;
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.updateViewState(() => {
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.updateViewState(() => {
10038
- this.updateProgress(time);
9903
+ this.options.ngZone.run(() => {
9904
+ this.time.set(UploadWidgetUtils.formatDuration(time));
10039
9905
  });
10040
9906
  });
10041
9907
  this.wavesurfer.on('finish', () => {
10042
- this.updateViewState(() => {
10043
- this.isPlaying = false;
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
- const confirmed = this.checkHasRecorded();
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.updateViewState(() => {
9930
+ this.options.ngZone.run(() => {
10066
9931
  this.microphoneError.set(false);
10067
- this.isWaveformOpen = true;
9932
+ this.isWaveformOpen.set(true);
10068
9933
  this.isRecording.set(true);
10069
- this.isPlaying = false;
9934
+ this.isPlaying.set(false);
10070
9935
  this.recordedBlob.set(null);
10071
- this.time = '00:00';
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.updateViewState(() => {
9959
+ this.options.ngZone.run(() => {
10095
9960
  this.isRecording.set(false);
10096
- this.isWaveformOpen = !!this.recordedBlob();
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.updateViewState(() => {
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.updateViewState(() => {
9982
+ this.options.ngZone.run(() => {
10131
9983
  this.isRecording.set(false);
10132
- this.isWaveformOpen = false;
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.recordingSaved.emit(file);
10002
+ this.options.handOver(file);
10151
10003
  // Destroy completely so next recording initializes fresh
10152
10004
  this.destroyRecorder();
10153
- this.updateViewState(() => {
10005
+ this.options.ngZone.run(() => {
10154
10006
  this.recordedBlob.set(null);
10155
- this.time = '00:00';
10156
- this.isPlaying = false;
10157
- this.isWaveformOpen = false;
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.updateViewState(() => {
10164
- this.isPlaying = !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
- ngOnDestroy() {
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
- updateViewState(update) {
10195
- this.ngZone.run(() => {
10196
- update();
10197
- this.changeDetectorRef.markForCheck();
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
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: VoiceRecordWidgetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
10201
- 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 }, actionsInHost: { classPropertyName: "actionsInHost", publicName: "actionsInHost", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { recordingSaved: "recordingSaved" }, host: { properties: { "class.actionsInHost": "actionsInHost()" } }, 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\"\n [class.direction-left]=\"isLeftDirection\"\n [class.direction-right]=\"!isLeftDirection\"\n [class.active]=\"isWaveformOpen || isRecording() || !!recordedBlob()\"\n [class.isRecording]=\"isRecording()\"\n [class.hasRecording]=\"recordedBlob()\"\n >\n <div class=\"recorderMain\">\n <div class=\"recorderToolbar\">\n <!-- Start Recording -->\n @if (!isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"micButton\"\n (actionClick)=\"requestRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Stop Recording -->\n @if (isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"effectiveStopButton()\"\n (actionClick)=\"stopRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Save Recording -->\n @if (recordedBlob() && !actionsInHost()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"effectiveSaveButton()\"\n (actionClick)=\"saveRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Start Play -->\n @if (recordedBlob() && !isPlaying) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"playButton\"\n (actionClick)=\"replayRecording()\"\n ></ui-action-button>\n }\n\n <!-- Stop Play -->\n @if (recordedBlob() && isPlaying) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"stopPlaybackButton\"\n (actionClick)=\"replayRecording()\"\n ></ui-action-button>\n }\n\n @if (isRecording()) {\n <div class=\"progress\">{{ time }}</div>\n }\n </div>\n\n <div class=\"waveformBand\" [class.open]=\"isWaveformOpen || isRecording() || !!recordedBlob()\">\n <div class=\"waveformGrid\"></div>\n <div class=\"waveform\" #waveform></div>\n </div>\n </div>\n</div>\n@if (showMicrophoneError() && microphoneError()) {\n <p class=\"message\">{{ texts().microphoneError }}</p>\n @if (actionsInHost()) {\n <p class=\"hint\">{{ texts().microphoneRetryHint }}</p>\n }\n}\n@if (actionsInHost() && recordedBlob()) {\n <p class=\"hint\">{{ texts().recordingHint }}</p>\n}\n\n<ng-template #actionButtons>\n @if (isRecording()) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"stopRecording()\"\n >\n </ui-action-button>\n }\n @if (recordedBlob()) {\n <ui-action-button\n class=\"saveRecordingButton\"\n [descriptor]=\"effectiveSaveButton()\"\n (actionClick)=\"saveRecording()\"\n >\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}:host(.actionsInHost) .message{color:var(--warn-color)}:host(.actionsInHost) .hint{margin:.5rem 0 0;text-align:center;font-size:.75rem;color:var(--color-text-muted)}:host(.actionsInHost) .progress{color:var(--color-text-muted)}:host(.actionsInHost) .isRecording .progress:before{content:\"\";display:inline-block;width:.5rem;height:.5rem;margin-right:.3125rem;border-radius:50%;background:var(--warn-color)}:host(.actionsInHost) .active ::ng-deep .recorderToolbar{border-color:var(--color-border)}:host(.actionsInHost) .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(.actionsInHost) .recorderToolbar ::ng-deep .sb4-warn,:host(.actionsInHost) .recorderToolbar ::ng-deep .sb4-primary,:host(.actionsInHost) .recorderToolbar ::ng-deep ui-action-button{border:unset}:host(.actionsInHost) .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"] }] }); }
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: VoiceRecordWidgetComponent, decorators: [{
10098
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: CaptureRecorderViewComponent, decorators: [{
10204
10099
  type: Component,
10205
- args: [{ selector: 'voice-record-widget', imports: [UiActionButtonComponent], host: { '[class.actionsInHost]': 'actionsInHost()' }, template: "<div\n class=\"recorderContainer\"\n [class.direction-left]=\"isLeftDirection\"\n [class.direction-right]=\"!isLeftDirection\"\n [class.active]=\"isWaveformOpen || isRecording() || !!recordedBlob()\"\n [class.isRecording]=\"isRecording()\"\n [class.hasRecording]=\"recordedBlob()\"\n >\n <div class=\"recorderMain\">\n <div class=\"recorderToolbar\">\n <!-- Start Recording -->\n @if (!isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"micButton\"\n (actionClick)=\"requestRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Stop Recording -->\n @if (isRecording()) {\n <ui-action-button\n class=\"mainButton\"\n #mainButton\n [descriptor]=\"effectiveStopButton()\"\n (actionClick)=\"stopRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Save Recording -->\n @if (recordedBlob() && !actionsInHost()) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"effectiveSaveButton()\"\n (actionClick)=\"saveRecording()\"\n >\n </ui-action-button>\n }\n\n <!-- Start Play -->\n @if (recordedBlob() && !isPlaying) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"playButton\"\n (actionClick)=\"replayRecording()\"\n ></ui-action-button>\n }\n\n <!-- Stop Play -->\n @if (recordedBlob() && isPlaying) {\n <ui-action-button\n class=\"mainButton\"\n [descriptor]=\"stopPlaybackButton\"\n (actionClick)=\"replayRecording()\"\n ></ui-action-button>\n }\n\n @if (isRecording()) {\n <div class=\"progress\">{{ time }}</div>\n }\n </div>\n\n <div class=\"waveformBand\" [class.open]=\"isWaveformOpen || isRecording() || !!recordedBlob()\">\n <div class=\"waveformGrid\"></div>\n <div class=\"waveform\" #waveform></div>\n </div>\n </div>\n</div>\n@if (showMicrophoneError() && microphoneError()) {\n <p class=\"message\">{{ texts().microphoneError }}</p>\n @if (actionsInHost()) {\n <p class=\"hint\">{{ texts().microphoneRetryHint }}</p>\n }\n}\n@if (actionsInHost() && recordedBlob()) {\n <p class=\"hint\">{{ texts().recordingHint }}</p>\n}\n\n<ng-template #actionButtons>\n @if (isRecording()) {\n <ui-action-button\n class=\"stopRecordingButton\"\n [descriptor]=\"stopRecordingButton()\"\n (actionClick)=\"stopRecording()\"\n >\n </ui-action-button>\n }\n @if (recordedBlob()) {\n <ui-action-button\n class=\"saveRecordingButton\"\n [descriptor]=\"effectiveSaveButton()\"\n (actionClick)=\"saveRecording()\"\n >\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}:host(.actionsInHost) .message{color:var(--warn-color)}:host(.actionsInHost) .hint{margin:.5rem 0 0;text-align:center;font-size:.75rem;color:var(--color-text-muted)}:host(.actionsInHost) .progress{color:var(--color-text-muted)}:host(.actionsInHost) .isRecording .progress:before{content:\"\";display:inline-block;width:.5rem;height:.5rem;margin-right:.3125rem;border-radius:50%;background:var(--warn-color)}:host(.actionsInHost) .active ::ng-deep .recorderToolbar{border-color:var(--color-border)}:host(.actionsInHost) .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(.actionsInHost) .recorderToolbar ::ng-deep .sb4-warn,:host(.actionsInHost) .recorderToolbar ::ng-deep .sb4-primary,:host(.actionsInHost) .recorderToolbar ::ng-deep ui-action-button{border:unset}:host(.actionsInHost) .mainButton ::ng-deep .sb4-warn{padding:.5rem .75rem}\n"] }]
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 }] }], 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 }] }], actionsInHost: [{ type: i0.Input, args: [{ isSignal: true, alias: "actionsInHost", required: false }] }], recordingSaved: [{ type: i0.Output, args: ["recordingSaved"] }] } });
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.cameraWidget = viewChild(PhotoCaptureWidgetComponent, /* @ts-ignore */
10222
- ...(ngDevMode ? [{ debugName: "cameraWidget" }] : /* istanbul ignore next */ []));
10223
- this.recorderWidget = viewChild(VoiceRecordWidgetComponent, /* @ts-ignore */
10224
- ...(ngDevMode ? [{ debugName: "recorderWidget" }] : /* istanbul ignore next */ []));
10225
- this.captureWidget = computed(() => this.cameraWidget() ?? this.recorderWidget(), /* @ts-ignore */
10226
- ...(ngDevMode ? [{ debugName: "captureWidget" }] : /* istanbul ignore next */ []));
10227
- /** The widget's buttons go to the footer of the dialog. */
10228
- this.actionButtons = computed(() => this.captureWidget()?.actionButtonsTemplate() ?? null, /* @ts-ignore */
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.captureWidget()?.isCaptureUnderWay() ?? false;
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 widget, which stops the device. */
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: "cameraWidget", first: true, predicate: PhotoCaptureWidgetComponent, descendants: true, isSignal: true }, { propertyName: "recorderWidget", first: true, predicate: VoiceRecordWidgetComponent, 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 <voice-record-widget\n [deviceStart]=\"data.settings.deviceStart\"\n [handover]=\"data.settings.handover\"\n [actionsInHost]=\"true\"\n [showSaveTitle]=\"true\"\n [texts]=\"data.texts\"\n [showMicrophoneError]=\"true\"\n (recordingSaved)=\"handOverCapture([$event])\"\n ></voice-record-widget>\n }\n </div>\n } @else {\n <photo-capture-widget\n [mode]=\"cameraMode\"\n [deviceStart]=\"data.settings.deviceStart\"\n [handover]=\"data.settings.handover\"\n [actionsInHost]=\"true\"\n [texts]=\"data.texts\"\n [maxFileSize]=\"data.maxFileSize\"\n (photoCaptured)=\"handOverCapture($event)\"\n ></photo-capture-widget>\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}.captureDialog ::ng-deep .stopRecordingButton{--text-color: color-mix(in srgb, var(--warn-color) 5%, white)}photo-capture-widget{display:block;width:100%}.recorderSlot{min-height:3.25rem}\n"], dependencies: [{ 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: PhotoCaptureWidgetComponent, selector: "photo-capture-widget", inputs: ["maxFileSize", "mode", "externalTrigger", "texts", "deviceStart", "handover", "actionsInHost"], outputs: ["photoCaptured"] }, { kind: "component", type: UiActionButtonComponent, selector: "ui-action-button", inputs: ["disabled", "descriptor", "code", "identifier", "viewActivated", "tab", "rowState", "addedCssClass", "addBasicClasses", "iconPlaceholder", "chords"], outputs: ["actionClick", "actionDoubleClick"] }, { kind: "component", type: VoiceRecordWidgetComponent, selector: "voice-record-widget", inputs: ["openDirection", "texts", "showMicrophoneError", "deviceStart", "handover", "showSaveTitle", "actionsInHost"], outputs: ["recordingSaved"] }] }); }
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
- VoiceRecordWidgetComponent,
10274
- ], 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 <voice-record-widget\n [deviceStart]=\"data.settings.deviceStart\"\n [handover]=\"data.settings.handover\"\n [actionsInHost]=\"true\"\n [showSaveTitle]=\"true\"\n [texts]=\"data.texts\"\n [showMicrophoneError]=\"true\"\n (recordingSaved)=\"handOverCapture([$event])\"\n ></voice-record-widget>\n }\n </div>\n } @else {\n <photo-capture-widget\n [mode]=\"cameraMode\"\n [deviceStart]=\"data.settings.deviceStart\"\n [handover]=\"data.settings.handover\"\n [actionsInHost]=\"true\"\n [texts]=\"data.texts\"\n [maxFileSize]=\"data.maxFileSize\"\n (photoCaptured)=\"handOverCapture($event)\"\n ></photo-capture-widget>\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}.captureDialog ::ng-deep .stopRecordingButton{--text-color: color-mix(in srgb, var(--warn-color) 5%, white)}photo-capture-widget{display:block;width:100%}.recorderSlot{min-height:3.25rem}\n"] }]
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", "actionsInHost"], outputs: ["photoCaptured"] }, { kind: "component", type: VoiceRecordWidgetComponent, selector: "voice-record-widget", inputs: ["openDirection", "texts", "showMicrophoneError", "deviceStart", "handover", "showSaveTitle", "actionsInHost"], 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"] }] }); }
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", "actionsInHost"], outputs: ["recordingSaved"] }] }); }
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
  }