@servicemind.tis/angular-image-and-file-picker 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/fesm2022/servicemind.tis-angular-image-and-file-picker.mjs +3658 -0
  4. package/fesm2022/servicemind.tis-angular-image-and-file-picker.mjs.map +1 -0
  5. package/index.d.ts +5 -0
  6. package/lib/angular-camera-capture-dialog/angular-camera-capture-dialog.component.d.ts +34 -0
  7. package/lib/angular-confirmation-dialog/angular-confirmation-dialog.component.d.ts +10 -0
  8. package/lib/angular-error-dialog/angular-error-dialog.component.d.ts +10 -0
  9. package/lib/angular-file-viewer/angular-excel-file-viewer/angular-excel-file-viewer.component.d.ts +14 -0
  10. package/lib/angular-file-viewer/angular-file-viewer.component.d.ts +16 -0
  11. package/lib/angular-file-viewer/angular-pdf-viewer/angular-pdf-viewer.component.d.ts +16 -0
  12. package/lib/angular-file-viewer/angular-video/angular-video.component.d.ts +13 -0
  13. package/lib/angular-image-and-file-picker/angular-image-and-file-picker.component.d.ts +186 -0
  14. package/lib/angular-image-and-file-picker.module.d.ts +31 -0
  15. package/lib/angular-image-and-file-picker.service.d.ts +6 -0
  16. package/lib/angular-preview-image/angular-preview-image.component.d.ts +19 -0
  17. package/lib/angular-qr-code-dialog/angular-qr-code-dialog.component.d.ts +90 -0
  18. package/lib/angular-view-connection-dialog/angular-view-connection-dialog.component.d.ts +44 -0
  19. package/lib/interfaces/config.type.d.ts +19 -0
  20. package/lib/interfaces/file-viewer-file-type.type.d.ts +41 -0
  21. package/lib/interfaces/index.d.ts +3 -0
  22. package/lib/interfaces/socket-adapter.interface.d.ts +315 -0
  23. package/lib/pipes/time-ago.pipe.d.ts +7 -0
  24. package/lib/services/angular-helper.service.d.ts +45 -0
  25. package/lib/services/angular-remote-upload.service.d.ts +273 -0
  26. package/package.json +53 -0
  27. package/public-api.d.ts +7 -0
@@ -0,0 +1,3658 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Injectable, ViewChild, Input, ChangeDetectionStrategy, Component, Inject, Pipe, signal, EventEmitter, Output, NgModule } from '@angular/core';
3
+ import * as i1$2 from '@angular/material/dialog';
4
+ import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
5
+ import * as i1$1 from '@angular/platform-browser';
6
+ import * as i1 from 'ngx-extended-pdf-viewer';
7
+ import { NgxExtendedPdfViewerComponent, NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer';
8
+ import * as i4 from '@angular/material/progress-spinner';
9
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
10
+ import * as i3 from '@angular/material/tooltip';
11
+ import { MatTooltipModule } from '@angular/material/tooltip';
12
+ import * as i3$1 from '@angular/material/icon';
13
+ import { MatIconModule } from '@angular/material/icon';
14
+ import * as i4$1 from '@angular/material/button';
15
+ import { MatButtonModule } from '@angular/material/button';
16
+ import * as i3$2 from '@angular/common';
17
+ import { CommonModule } from '@angular/common';
18
+ import { Subject, BehaviorSubject, interval, takeUntil as takeUntil$1, map, shareReplay } from 'rxjs';
19
+ import * as i3$4 from '@angular/cdk/layout';
20
+ import { Breakpoints } from '@angular/cdk/layout';
21
+ import * as i3$3 from '@angular/common/http';
22
+ import { HttpHeaders, HttpClientModule } from '@angular/common/http';
23
+ import * as i1$3 from '@angular/material/snack-bar';
24
+ import { MatSnackBarModule } from '@angular/material/snack-bar';
25
+ import imageCompression from 'browser-image-compression';
26
+ import { takeUntil, take, timeout } from 'rxjs/operators';
27
+ import * as i4$2 from 'angularx-qrcode';
28
+ import { QRCodeComponent } from 'angularx-qrcode';
29
+ import * as i7 from '@angular/material/divider';
30
+ import { MatDividerModule } from '@angular/material/divider';
31
+ import * as i10 from '@angular/cdk/drag-drop';
32
+ import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
33
+ import * as i6 from '@angular/forms';
34
+ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
35
+ import { MatInputModule } from '@angular/material/input';
36
+
37
+ class AngularImageAndFilePickerService {
38
+ constructor() { }
39
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
40
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerService, providedIn: 'root' });
41
+ }
42
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerService, decorators: [{
43
+ type: Injectable,
44
+ args: [{
45
+ providedIn: 'root'
46
+ }]
47
+ }], ctorParameters: () => [] });
48
+
49
+ class AngularPdfViewerComponent {
50
+ cdRef;
51
+ loading = true;
52
+ isError = false;
53
+ src;
54
+ pdfViewer;
55
+ constructor(cdRef) {
56
+ this.cdRef = cdRef;
57
+ }
58
+ ngOnChanges(changes) {
59
+ if (changes['src']) {
60
+ console.log('Pdf Viewer: src changed: ', changes['src']);
61
+ }
62
+ }
63
+ destroyPdfViewer() {
64
+ this.pdfViewer.ngOnDestroy();
65
+ }
66
+ loadingFinished(ev) {
67
+ console.log('PDF Loaded', ev);
68
+ this.loading = false;
69
+ this.cdRef.detectChanges();
70
+ this.pdfViewer.height = 'auto';
71
+ }
72
+ loadingError(ev) {
73
+ console.log('PDF Loading Error:', ev);
74
+ this.isError = true;
75
+ this.loading = false;
76
+ this.cdRef.detectChanges();
77
+ }
78
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularPdfViewerComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
79
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: AngularPdfViewerComponent, isStandalone: false, selector: "angular-pdf-viewer", inputs: { src: "src" }, viewQueries: [{ propertyName: "pdfViewer", first: true, predicate: NgxExtendedPdfViewerComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "@if (isError) {\n <div class=\"flex angular-justify-content-center py-4\">\n <h3> Some Error Occurred in Loading PDF File. Please Try again. </h3>\n </div>\n} \n\n@if (!isError) {\n <div class=\"angular-extended-pdf-viewer\">\n @if(loading) {\n <div class=\"loading\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"50\"></mat-progress-spinner>\n </div>\n }\n <ngx-extended-pdf-viewer [src]=\"src\"\n [showToolbar]=\"true\"\n [showSidebarButton]=\"true\"\n [showFindButton]=\"false\"\n [showPagingButtons]=\"true\"\n [showDrawEditor]=\"false\"\n [showStampEditor]=\"false\"\n [showTextEditor]=\"false\"\n [showZoomButtons]=\"true\"\n [showPresentationModeButton]=\"false\"\n [showOpenFileButton]=\"false\"\n [showPrintButton]=\"true\"\n [showDownloadButton]=\"true\"\n [showSecondaryToolbarButton]=\"false\"\n [showRotateButton]=\"true\"\n [showHandToolButton]=\"false\"\n [showScrollingButton]=\"true\"\n [showSpreadButton]=\"false\"\n [showPropertiesButton]=\"false\"\n [zoom]=\"'page-fit'\"\n (pdfDownloaded)=\"loadingFinished($event)\"\n (pdfLoaded)=\"loadingFinished($event)\"\n (pdfLoadingFailed)=\"loadingError($event)\"\n >\n </ngx-extended-pdf-viewer>\n </div>\n}\n \n ", styles: ["::ng-deep .angular-extended-pdf-viewer{position:relative}::ng-deep .angular-extended-pdf-viewer .loading{height:98vh;background-color:#9e9e9e3d;width:100%;position:absolute;top:0;z-index:1;display:flex;justify-content:center;align-items:center}::ng-deep .angular-extended-pdf-viewer .zoom{height:98vh!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer{background-color:var(--angular-primary)!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-toggle-sidebar pdf-shy-button .toolbarButton,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area pdf-shy-button .toolbarButton,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerMiddle pdf-shy-button .toolbarButton,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerRight pdf-shy-button .toolbarButton{display:flex;align-items:center!important;justify-content:center!important;color:#fff!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-toggle-sidebar pdf-shy-button .always-in-secondary-menu,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area pdf-shy-button .always-in-secondary-menu,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerMiddle pdf-shy-button .always-in-secondary-menu,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerRight pdf-shy-button .always-in-secondary-menu{display:none!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area .paging-left,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area .paging-right{display:inline-flex!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area pdf-page-number #pageNumber{margin:6px 0!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerRight #editorModeButtons{display:inline-flex!important}\n"], dependencies: [{ kind: "component", type: i1.NgxExtendedPdfViewerComponent, selector: "ngx-extended-pdf-viewer", inputs: ["customFindbarInputArea", "customToolbar", "customFindbar", "customFindbarButtons", "customPdfViewer", "customSecondaryToolbar", "customSidebar", "customThumbnail", "customFreeFloatingBar", "showFreeFloatingBar", "enableDragAndDrop", "formData", "disableForms", "pageViewMode", "scrollMode", "authorization", "httpHeaders", "contextMenuAllowed", "enablePrint", "delayFirstView", "showTextEditor", "showStampEditor", "showDrawEditor", "showHighlightEditor", "logLevel", "relativeCoordsOptions", "minifiedJSLibraries", "printResolution", "rotation", "src", "base64Src", "minHeight", "height", "forceUsingLegacyES5", "backgroundColor", "filenameForDownload", "ignoreKeyboard", "ignoreKeys", "acceptKeys", "imageResourcesPath", "localeFolderPath", "language", "listenToURL", "nameddest", "password", "replaceBrowserPrint", "showUnverifiedSignatures", "startTabindex", "showSidebarButton", "sidebarVisible", "activeSidebarView", "findbarVisible", "propertiesDialogVisible", "showFindButton", "showFindHighlightAll", "showFindMatchCase", "showFindCurrentPageOnly", "showFindPageRange", "showFindEntireWord", "showFindEntirePhrase", "showFindMatchDiacritics", "showFindFuzzySearch", "showFindResultsCount", "showFindMessages", "showPagingButtons", "showZoomButtons", "showPresentationModeButton", "showOpenFileButton", "showPrintButton", "showDownloadButton", "theme", "showToolbar", "showSecondaryToolbarButton", "showSinglePageModeButton", "showVerticalScrollButton", "showHorizontalScrollButton", "showWrappedScrollButton", "showInfiniteScrollButton", "showBookModeButton", "showRotateButton", "showRotateCwButton", "showRotateCcwButton", "handTool", "showHandToolButton", "showScrollingButton", "showSpreadButton", "showPropertiesButton", "showBorders", "spread", "page", "pageLabel", "textLayer", "zoom", "zoomLevels", "maxZoom", "minZoom", "mobileFriendlyZoom"], outputs: ["annotationEditorEvent", "formDataChange", "pageViewModeChange", "progress", "srcChange", "scrollModeChange", "afterPrint", "beforePrint", "currentZoomFactor", "rotationChange", "annotationLayerRendered", "annotationEditorLayerRendered", "xfaLayerRendered", "outlineLoaded", "attachmentsloaded", "layersloaded", "sidebarVisibleChange", "activeSidebarViewChange", "findbarVisibleChange", "propertiesDialogVisibleChange", "handToolChange", "spreadChange", "thumbnailDrawn", "pageChange", "pageLabelChange", "pagesLoaded", "pageRender", "pageRendered", "pdfDownloaded", "pdfLoaded", "pdfLoadingStarts", "pdfLoadingFailed", "textLayerRendered", "annotationEditorModeChanged", "updateFindMatchesCount", "updateFindState", "zoomChange"] }, { kind: "component", type: i4.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
80
+ }
81
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularPdfViewerComponent, decorators: [{
82
+ type: Component,
83
+ args: [{ selector: 'angular-pdf-viewer', standalone: false, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (isError) {\n <div class=\"flex angular-justify-content-center py-4\">\n <h3> Some Error Occurred in Loading PDF File. Please Try again. </h3>\n </div>\n} \n\n@if (!isError) {\n <div class=\"angular-extended-pdf-viewer\">\n @if(loading) {\n <div class=\"loading\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"50\"></mat-progress-spinner>\n </div>\n }\n <ngx-extended-pdf-viewer [src]=\"src\"\n [showToolbar]=\"true\"\n [showSidebarButton]=\"true\"\n [showFindButton]=\"false\"\n [showPagingButtons]=\"true\"\n [showDrawEditor]=\"false\"\n [showStampEditor]=\"false\"\n [showTextEditor]=\"false\"\n [showZoomButtons]=\"true\"\n [showPresentationModeButton]=\"false\"\n [showOpenFileButton]=\"false\"\n [showPrintButton]=\"true\"\n [showDownloadButton]=\"true\"\n [showSecondaryToolbarButton]=\"false\"\n [showRotateButton]=\"true\"\n [showHandToolButton]=\"false\"\n [showScrollingButton]=\"true\"\n [showSpreadButton]=\"false\"\n [showPropertiesButton]=\"false\"\n [zoom]=\"'page-fit'\"\n (pdfDownloaded)=\"loadingFinished($event)\"\n (pdfLoaded)=\"loadingFinished($event)\"\n (pdfLoadingFailed)=\"loadingError($event)\"\n >\n </ngx-extended-pdf-viewer>\n </div>\n}\n \n ", styles: ["::ng-deep .angular-extended-pdf-viewer{position:relative}::ng-deep .angular-extended-pdf-viewer .loading{height:98vh;background-color:#9e9e9e3d;width:100%;position:absolute;top:0;z-index:1;display:flex;justify-content:center;align-items:center}::ng-deep .angular-extended-pdf-viewer .zoom{height:98vh!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer{background-color:var(--angular-primary)!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-toggle-sidebar pdf-shy-button .toolbarButton,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area pdf-shy-button .toolbarButton,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerMiddle pdf-shy-button .toolbarButton,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerRight pdf-shy-button .toolbarButton{display:flex;align-items:center!important;justify-content:center!important;color:#fff!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-toggle-sidebar pdf-shy-button .always-in-secondary-menu,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area pdf-shy-button .always-in-secondary-menu,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerMiddle pdf-shy-button .always-in-secondary-menu,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerRight pdf-shy-button .always-in-secondary-menu{display:none!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area .paging-left,::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area .paging-right{display:inline-flex!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerLeft pdf-paging-area pdf-page-number #pageNumber{margin:6px 0!important}::ng-deep .angular-extended-pdf-viewer .zoom pdf-toolbar .toolbar #toolbarContainer #toolbarViewer #toolbarViewerRight #editorModeButtons{display:inline-flex!important}\n"] }]
84
+ }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { src: [{
85
+ type: Input,
86
+ args: [{ required: true }]
87
+ }], pdfViewer: [{
88
+ type: ViewChild,
89
+ args: [NgxExtendedPdfViewerComponent, { static: false }]
90
+ }] } });
91
+
92
+ class AngularExcelFileViewerComponent {
93
+ sanitizer;
94
+ src;
95
+ baseUrl = `https://view.officeapps.live.com/op/embed.aspx?src=`;
96
+ iframeSrc = null;
97
+ constructor(sanitizer) {
98
+ this.sanitizer = sanitizer;
99
+ }
100
+ ngOnChanges(changes) {
101
+ if (changes['src']) {
102
+ console.log('Url Received', this.src);
103
+ const src = this.baseUrl + this.src;
104
+ this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(src);
105
+ console.log('Iframe src:', this.iframeSrc);
106
+ }
107
+ }
108
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularExcelFileViewerComponent, deps: [{ token: i1$1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
109
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: AngularExcelFileViewerComponent, isStandalone: false, selector: "angular-excel-file-viewer", inputs: { src: "src" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"bg-slate-900 w-full h-5\">\n \n</div>\n<div class=\"aspect-w-16 aspect-h-9\">\n <iframe class=\"w-full h-full\" [src]=\"iframeSrc\" frameborder='0'>This is an embedded <a target='_blank'\n href='http://office.com'>Microsoft Office</a> document, powered by <a target='_blank'\n href='http://office.com/webapps'>Office Online</a>.</iframe>\n</div>", styles: ["iframe{height:95vh}\n"] });
110
+ }
111
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularExcelFileViewerComponent, decorators: [{
112
+ type: Component,
113
+ args: [{ selector: 'angular-excel-file-viewer', standalone: false, template: "<div class=\"bg-slate-900 w-full h-5\">\n \n</div>\n<div class=\"aspect-w-16 aspect-h-9\">\n <iframe class=\"w-full h-full\" [src]=\"iframeSrc\" frameborder='0'>This is an embedded <a target='_blank'\n href='http://office.com'>Microsoft Office</a> document, powered by <a target='_blank'\n href='http://office.com/webapps'>Office Online</a>.</iframe>\n</div>", styles: ["iframe{height:95vh}\n"] }]
114
+ }], ctorParameters: () => [{ type: i1$1.DomSanitizer }], propDecorators: { src: [{
115
+ type: Input,
116
+ args: [{ required: true }]
117
+ }] } });
118
+
119
+ class AngularVideoComponent {
120
+ src;
121
+ type;
122
+ constructor() { }
123
+ ngOnInit() {
124
+ if (this.src) {
125
+ this.type = this.getFileType(String(this.src));
126
+ }
127
+ }
128
+ ngOnChanges(changes) {
129
+ if (changes['src']) {
130
+ console.log('Pdf Viewer: src changed: ', changes['src']);
131
+ this.type = this.getFileType(changes['src'].currentValue);
132
+ }
133
+ }
134
+ getFileType(text) {
135
+ let txtArr = text.split('.');
136
+ let fileType = txtArr[txtArr.length - 1];
137
+ return fileType.toLocaleLowerCase() == 'mov' ? 'mp4' : fileType.toLocaleLowerCase();
138
+ }
139
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularVideoComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
140
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: AngularVideoComponent, isStandalone: false, selector: "angular-video", inputs: { src: "src" }, usesOnChanges: true, ngImport: i0, template: "<video controls autoplay style=\"min-width: 300px; max-width: 90vw; min-height: 300px; max-height: 90vh;\">\n <source [src]=\"src\" type=\"video/{{type}}\">\n Your browser does not support the video tag.\n</video>", styles: [""] });
141
+ }
142
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularVideoComponent, decorators: [{
143
+ type: Component,
144
+ args: [{ selector: 'angular-video', standalone: false, template: "<video controls autoplay style=\"min-width: 300px; max-width: 90vw; min-height: 300px; max-height: 90vh;\">\n <source [src]=\"src\" type=\"video/{{type}}\">\n Your browser does not support the video tag.\n</video>" }]
145
+ }], ctorParameters: () => [], propDecorators: { src: [{
146
+ type: Input,
147
+ args: [{ required: true }]
148
+ }] } });
149
+
150
+ class AngularFileViewerComponent {
151
+ dialogRef;
152
+ data;
153
+ sanitizer;
154
+ safeUrl;
155
+ pdfViewer;
156
+ constructor(dialogRef, data, sanitizer) {
157
+ this.dialogRef = dialogRef;
158
+ this.data = data;
159
+ this.sanitizer = sanitizer;
160
+ console.log('File Viewer Data:', data);
161
+ this.safeUrl = this.data.src;
162
+ console.log('FileViewer SafeUrl:', this.safeUrl);
163
+ dialogRef.beforeClosed().subscribe((result) => {
164
+ console.log('The dialog is about to be closed');
165
+ // Here's the interesting bit:
166
+ if (this.pdfViewer) {
167
+ this.pdfViewer.destroyPdfViewer();
168
+ console.log('PDF Viewer Destroyed.');
169
+ }
170
+ });
171
+ }
172
+ downloadFile() {
173
+ var el = document.createElement("a");
174
+ el.setAttribute("href", this.safeUrl);
175
+ el.setAttribute("download", 'Video-' + new Date().valueOf());
176
+ document.body.appendChild(el);
177
+ el.click();
178
+ el.remove();
179
+ }
180
+ onClose(status) {
181
+ this.dialogRef.close(status);
182
+ }
183
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularFileViewerComponent, deps: [{ token: i1$2.MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: i1$1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Component });
184
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: AngularFileViewerComponent, isStandalone: false, selector: "angular-file-viewer", viewQueries: [{ propertyName: "pdfViewer", first: true, predicate: AngularPdfViewerComponent, descendants: true }], ngImport: i0, template: "@if(safeUrl) {\n <div class=\"angular-pdf-viewer\">\n @if(data.fileType == 'video') {\n <mat-icon class=\"download-btn\" (click)=\"downloadFile()\" matTooltip=\"Download\">download</mat-icon>\n <!-- <button mat-icon-button color=\"primary\" class=\"download-btn\" >\n <mat-icon>download</mat-icon>\n </button> -->\n }\n @else {\n <button mat-mini-fab color=\"primary\" class=\"close-btn\" (click)=\"onClose(null)\" matTooltip=\"Close\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n @if(data.fileType == 'pdf') {\n <angular-pdf-viewer [src]=\"safeUrl\"></angular-pdf-viewer>\n }\n @else if(data.fileType == 'excel') {\n <angular-excel-file-viewer [src]=\"safeUrl\"></angular-excel-file-viewer>\n }\n @else if(data.fileType == 'video') {\n <angular-video [src]=\"safeUrl\"></angular-video>\n }\n </div>\n}\n", styles: [".angular-pdf-viewer{position:relative!important;height:100%}.angular-pdf-viewer .close-btn{position:absolute!important;bottom:10px!important;right:10px!important;z-index:1!important}.angular-pdf-viewer .download-btn{color:var(--angular-primary);cursor:pointer;position:absolute!important;top:10px!important;right:10px!important;z-index:1!important}\n"], dependencies: [{ kind: "directive", type: i3.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i4$1.MatMiniFabButton, selector: "button[mat-mini-fab]", exportAs: ["matButton"] }, { kind: "component", type: AngularExcelFileViewerComponent, selector: "angular-excel-file-viewer", inputs: ["src"] }, { kind: "component", type: AngularPdfViewerComponent, selector: "angular-pdf-viewer", inputs: ["src"] }, { kind: "component", type: AngularVideoComponent, selector: "angular-video", inputs: ["src"] }] });
185
+ }
186
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularFileViewerComponent, decorators: [{
187
+ type: Component,
188
+ args: [{ selector: 'angular-file-viewer', standalone: false, template: "@if(safeUrl) {\n <div class=\"angular-pdf-viewer\">\n @if(data.fileType == 'video') {\n <mat-icon class=\"download-btn\" (click)=\"downloadFile()\" matTooltip=\"Download\">download</mat-icon>\n <!-- <button mat-icon-button color=\"primary\" class=\"download-btn\" >\n <mat-icon>download</mat-icon>\n </button> -->\n }\n @else {\n <button mat-mini-fab color=\"primary\" class=\"close-btn\" (click)=\"onClose(null)\" matTooltip=\"Close\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n @if(data.fileType == 'pdf') {\n <angular-pdf-viewer [src]=\"safeUrl\"></angular-pdf-viewer>\n }\n @else if(data.fileType == 'excel') {\n <angular-excel-file-viewer [src]=\"safeUrl\"></angular-excel-file-viewer>\n }\n @else if(data.fileType == 'video') {\n <angular-video [src]=\"safeUrl\"></angular-video>\n }\n </div>\n}\n", styles: [".angular-pdf-viewer{position:relative!important;height:100%}.angular-pdf-viewer .close-btn{position:absolute!important;bottom:10px!important;right:10px!important;z-index:1!important}.angular-pdf-viewer .download-btn{color:var(--angular-primary);cursor:pointer;position:absolute!important;top:10px!important;right:10px!important;z-index:1!important}\n"] }]
189
+ }], ctorParameters: () => [{ type: i1$2.MatDialogRef }, { type: undefined, decorators: [{
190
+ type: Inject,
191
+ args: [MAT_DIALOG_DATA]
192
+ }] }, { type: i1$1.DomSanitizer }], propDecorators: { pdfViewer: [{
193
+ type: ViewChild,
194
+ args: [AngularPdfViewerComponent, { static: false }]
195
+ }] } });
196
+
197
+ class AngularPreviewImageComponent {
198
+ dialogRef;
199
+ data;
200
+ width = 850;
201
+ height = 650;
202
+ isLoading = true;
203
+ isLandscape;
204
+ orientation;
205
+ imageWidth;
206
+ imageHeight;
207
+ constructor(dialogRef, data) {
208
+ this.dialogRef = dialogRef;
209
+ this.data = data;
210
+ }
211
+ ngOnInit() {
212
+ if (this.data.url) {
213
+ setTimeout(() => {
214
+ this.checkImageOrientation(this.data.url);
215
+ }, 500);
216
+ }
217
+ }
218
+ checkImageOrientation(imageUrl) {
219
+ const img = new Image();
220
+ img.src = imageUrl;
221
+ this.isLoading = true;
222
+ img.onload = () => {
223
+ this.imageWidth = img.naturalWidth;
224
+ this.imageHeight = img.naturalHeight;
225
+ this.isLandscape = img.naturalWidth > img.naturalHeight;
226
+ this.orientation = this.isLandscape ? 'landscape-image' : 'portrait-image';
227
+ this.isLoading = false;
228
+ };
229
+ img.onerror = () => {
230
+ console.error('Failed to load image.');
231
+ this.isLoading = false;
232
+ };
233
+ }
234
+ onClose() {
235
+ this.dialogRef.close();
236
+ }
237
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularPreviewImageComponent, deps: [{ token: i1$2.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
238
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: AngularPreviewImageComponent, isStandalone: false, selector: "angular-preview-image", ngImport: i0, template: "@if(isLoading){\n <div style=\"width: 350px; height: 350px; display: flex; justify-content: center; align-items: center\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"80\"></mat-progress-spinner>\n </div>\n}\n@else{\n <div class=\"w-100 dialog-container\" appScrolling style=\"--img-width: {{(imageWidth && imageWidth < 700) ? imageWidth : 650}}px; --img-height: {{(imageHeight && imageHeight < 600) ? imageHeight : 600}}px\">\n <mat-icon\n class=\"close-icon\"\n (click)=\"onClose()\">cancel</mat-icon>\n <img \n *ngIf=\"data.url\" \n [src]=\"data.url\" \n alt=\"preview-image\" \n class=\"preview-image {{orientation}}\">\n </div>\n}", styles: ["::ng-deep .mat-p-0 .mat-dialog-container{padding:0!important}.dialog-container{overflow:auto;max-height:calc(100vh - 15px);display:flex;justify-content:center;align-items:center}.landscape-image{width:var(--img-width);max-width:95vw;object-fit:contain}.portrait-image{height:var(--img-height);max-height:95vh;object-fit:contain}.close-icon{color:var(--angular-cancel, #bb333b);position:absolute;top:10px;right:10px;z-index:99;cursor:pointer}\n"], dependencies: [{ kind: "directive", type: i3$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i4.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }] });
239
+ }
240
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularPreviewImageComponent, decorators: [{
241
+ type: Component,
242
+ args: [{ selector: 'angular-preview-image', standalone: false, template: "@if(isLoading){\n <div style=\"width: 350px; height: 350px; display: flex; justify-content: center; align-items: center\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"80\"></mat-progress-spinner>\n </div>\n}\n@else{\n <div class=\"w-100 dialog-container\" appScrolling style=\"--img-width: {{(imageWidth && imageWidth < 700) ? imageWidth : 650}}px; --img-height: {{(imageHeight && imageHeight < 600) ? imageHeight : 600}}px\">\n <mat-icon\n class=\"close-icon\"\n (click)=\"onClose()\">cancel</mat-icon>\n <img \n *ngIf=\"data.url\" \n [src]=\"data.url\" \n alt=\"preview-image\" \n class=\"preview-image {{orientation}}\">\n </div>\n}", styles: ["::ng-deep .mat-p-0 .mat-dialog-container{padding:0!important}.dialog-container{overflow:auto;max-height:calc(100vh - 15px);display:flex;justify-content:center;align-items:center}.landscape-image{width:var(--img-width);max-width:95vw;object-fit:contain}.portrait-image{height:var(--img-height);max-height:95vh;object-fit:contain}.close-icon{color:var(--angular-cancel, #bb333b);position:absolute;top:10px;right:10px;z-index:99;cursor:pointer}\n"] }]
243
+ }], ctorParameters: () => [{ type: i1$2.MatDialogRef }, { type: undefined, decorators: [{
244
+ type: Inject,
245
+ args: [MAT_DIALOG_DATA]
246
+ }] }] });
247
+
248
+ class AngularErrorDialogComponent {
249
+ dialogRef;
250
+ data;
251
+ constructor(dialogRef, data) {
252
+ this.dialogRef = dialogRef;
253
+ this.data = data;
254
+ }
255
+ onClose() {
256
+ this.dialogRef.close();
257
+ }
258
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularErrorDialogComponent, deps: [{ token: i1$2.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
259
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: AngularErrorDialogComponent, isStandalone: false, selector: "angular-error-dialog", ngImport: i0, template: "<h1 mat-dialog-title>\n <mat-icon class=\"{{data?.iconClass}}\">{{data?.icon}}</mat-icon>\n {{data?.title}}\n</h1>\n<div mat-dialog-content>\n {{data?.message}}\n</div>\n<div mat-dialog-actions>\n <button mat-button class=\"angular-text-upper {{data?.buttonClass ?? 'angular-btn-primary'}}\" (click)=\"onClose()\" cdkFocusInitial *ngIf=\"data?.buttonText && data?.buttonText != ''\">{{data?.buttonText}}</button>\n</div>", styles: [""], dependencies: [{ kind: "directive", type: i3$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i4$1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: i1$2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1$2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1$2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }] });
260
+ }
261
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularErrorDialogComponent, decorators: [{
262
+ type: Component,
263
+ args: [{ selector: 'angular-error-dialog', standalone: false, template: "<h1 mat-dialog-title>\n <mat-icon class=\"{{data?.iconClass}}\">{{data?.icon}}</mat-icon>\n {{data?.title}}\n</h1>\n<div mat-dialog-content>\n {{data?.message}}\n</div>\n<div mat-dialog-actions>\n <button mat-button class=\"angular-text-upper {{data?.buttonClass ?? 'angular-btn-primary'}}\" (click)=\"onClose()\" cdkFocusInitial *ngIf=\"data?.buttonText && data?.buttonText != ''\">{{data?.buttonText}}</button>\n</div>" }]
264
+ }], ctorParameters: () => [{ type: i1$2.MatDialogRef }, { type: undefined, decorators: [{
265
+ type: Inject,
266
+ args: [MAT_DIALOG_DATA]
267
+ }] }] });
268
+
269
+ // Maximum file size after compression (4MB)
270
+ const MAX_COMPRESSED_FILE_SIZE_MB = 4;
271
+ const MAX_COMPRESSED_FILE_SIZE_BYTES = MAX_COMPRESSED_FILE_SIZE_MB * 1024 * 1024;
272
+ // Quality levels for iterative compression (95%, 90%, 85%, ... down to 10%)
273
+ const QUALITY_LEVELS = [0.95, 0.90, 0.85, 0.80, 0.75, 0.70, 0.65, 0.60, 0.55, 0.50, 0.45, 0.40, 0.35, 0.30, 0.25, 0.20, 0.15, 0.10];
274
+ class AngularHelperService {
275
+ snackBar;
276
+ dialog;
277
+ http;
278
+ constructor(snackBar, dialog, http) {
279
+ this.snackBar = snackBar;
280
+ this.dialog = dialog;
281
+ this.http = http;
282
+ }
283
+ showHttpErrorMsg(error, duration = 5000) {
284
+ console.log('httpError: ', error);
285
+ let errorMessage = 'Some Unknown Error Occurred.';
286
+ let errorCode = 'Unknown Error';
287
+ const httpError = error;
288
+ if (httpError.status >= 400) {
289
+ const errorFromServer = httpError.error?.errors || httpError.error || [];
290
+ if (Array.isArray(errorFromServer) && errorFromServer.length > 0) {
291
+ errorMessage = errorFromServer[0].message;
292
+ errorCode = errorFromServer[0].code;
293
+ }
294
+ }
295
+ else if (httpError.status < 100) {
296
+ errorMessage = httpError.message;
297
+ errorCode = httpError.statusText;
298
+ }
299
+ if (errorCode == "VALIDATION_ERROR" || errorCode == "NOT_FOUND_ERROR" || errorCode == "THIRD_PARTY_API_ERROR") {
300
+ let confirmBoxData = {
301
+ title: "Error !",
302
+ message: errorMessage,
303
+ icon: "error",
304
+ iconClass: "angular-text-danger",
305
+ buttonText: "Ok",
306
+ buttonClass: "angular-btn-primary",
307
+ };
308
+ const dialogRef = this.dialog.open(AngularErrorDialogComponent, {
309
+ width: "550px",
310
+ panelClass: ['angular-simple-confirmation'],
311
+ data: confirmBoxData,
312
+ disableClose: false,
313
+ });
314
+ return dialogRef;
315
+ }
316
+ else {
317
+ const snackbarRef = this.snackBar.open(errorMessage, 'Error', { duration: duration });
318
+ return snackbarRef;
319
+ }
320
+ }
321
+ showSuccessMsg(message, title, duration = 5000) {
322
+ this.snackBar.open(message, title, {
323
+ duration: duration
324
+ });
325
+ }
326
+ showErrorMsg(message, title, duration = 5000) {
327
+ this.snackBar.open(message, title, {
328
+ duration: duration
329
+ });
330
+ }
331
+ /**
332
+ * Compresses an image file to ensure it's under MAX_COMPRESSED_FILE_SIZE_MB.
333
+ * Uses iterative compression with decreasing quality levels while maintaining aspect ratio.
334
+ * @param image - Data URL string or File object
335
+ * @param mimeType - The MIME type of the image (e.g., 'image/jpeg', 'image/png')
336
+ * @returns Promise<Blob> - The compressed image as a Blob
337
+ */
338
+ async compressFile(image, mimeType) {
339
+ const startTime = performance.now();
340
+ let file;
341
+ console.log('[ImageCompression] Starting compression process...');
342
+ console.log('[ImageCompression] Input type:', typeof image === 'string' ? 'DataURL' : image?.constructor?.name);
343
+ console.log('[ImageCompression] MIME type:', mimeType);
344
+ // Convert data URL to File if necessary
345
+ if (typeof image === 'string' && image.startsWith('data:')) {
346
+ const blob = this.dataURItoBlob(image.split(',')[1], mimeType);
347
+ file = new File([blob], 'image.' + this.getExtensionFromMimeType(mimeType), { type: mimeType });
348
+ console.log('[ImageCompression] Converted DataURL to File');
349
+ }
350
+ else if (image instanceof File) {
351
+ file = image;
352
+ console.log('[ImageCompression] Input is already a File:', file.name);
353
+ }
354
+ else if (image instanceof Blob) {
355
+ file = new File([image], 'image.' + this.getExtensionFromMimeType(mimeType), { type: mimeType });
356
+ console.log('[ImageCompression] Converted Blob to File');
357
+ }
358
+ else {
359
+ console.error('[ImageCompression] Invalid input type:', typeof image);
360
+ throw new Error('Invalid image input: expected data URL string, File, or Blob');
361
+ }
362
+ const originalSizeBytes = file.size;
363
+ const originalSizeKB = originalSizeBytes / 1024;
364
+ const originalSizeMB = originalSizeKB / 1024;
365
+ console.log('[ImageCompression] ═══════════════════════════════════════');
366
+ console.log('[ImageCompression] Original file size:', this.formatFileSize(originalSizeBytes));
367
+ console.log('[ImageCompression] Max allowed size:', `${MAX_COMPRESSED_FILE_SIZE_MB}MB`);
368
+ console.log('[ImageCompression] ═══════════════════════════════════════');
369
+ // If file is already under the limit, return as-is
370
+ if (file.size <= MAX_COMPRESSED_FILE_SIZE_BYTES) {
371
+ const elapsed = (performance.now() - startTime).toFixed(2);
372
+ console.log('[ImageCompression] ✓ File already under limit, no compression needed');
373
+ console.log(`[ImageCompression] Time elapsed: ${elapsed}ms`);
374
+ return file;
375
+ }
376
+ // Check if the file is an image that can be compressed
377
+ if (!this.isCompressibleImage(mimeType)) {
378
+ console.warn(`[ImageCompression] ⚠ File type ${mimeType} cannot be compressed. Returning original file.`);
379
+ return file;
380
+ }
381
+ console.log('[ImageCompression] Starting iterative compression...');
382
+ console.log('[ImageCompression] Quality levels to try:', QUALITY_LEVELS.map(q => `${q * 100}%`).join(', '));
383
+ // Iteratively compress with decreasing quality until under size limit
384
+ let compressedFile = file;
385
+ let attemptCount = 0;
386
+ for (const quality of QUALITY_LEVELS) {
387
+ attemptCount++;
388
+ try {
389
+ const options = {
390
+ maxSizeMB: MAX_COMPRESSED_FILE_SIZE_MB,
391
+ maxWidthOrHeight: undefined, // Maintain original dimensions (aspect ratio preserved)
392
+ useWebWorker: true,
393
+ initialQuality: quality,
394
+ preserveExif: true,
395
+ fileType: mimeType,
396
+ };
397
+ console.log(`[ImageCompression] Attempt ${attemptCount}: Quality ${quality * 100}%...`);
398
+ const attemptStart = performance.now();
399
+ compressedFile = await imageCompression(file, options);
400
+ const attemptTime = (performance.now() - attemptStart).toFixed(2);
401
+ const compressedSizeBytes = compressedFile.size;
402
+ const reductionPercent = ((1 - compressedSizeBytes / originalSizeBytes) * 100).toFixed(1);
403
+ console.log(`[ImageCompression] → Result: ${this.formatFileSize(compressedSizeBytes)} (${reductionPercent}% reduction) in ${attemptTime}ms`);
404
+ // If compressed file is under the limit, we're done
405
+ if (compressedFile.size <= MAX_COMPRESSED_FILE_SIZE_BYTES) {
406
+ const totalTime = (performance.now() - startTime).toFixed(2);
407
+ console.log('[ImageCompression] ═══════════════════════════════════════');
408
+ console.log('[ImageCompression] ✓ COMPRESSION SUCCESSFUL');
409
+ console.log(`[ImageCompression] Original size: ${this.formatFileSize(originalSizeBytes)}`);
410
+ console.log(`[ImageCompression] Compressed size: ${this.formatFileSize(compressedFile.size)}`);
411
+ console.log(`[ImageCompression] Size reduction: ${reductionPercent}%`);
412
+ console.log(`[ImageCompression] Final quality: ${quality * 100}%`);
413
+ console.log(`[ImageCompression] Attempts: ${attemptCount}`);
414
+ console.log(`[ImageCompression] Total time: ${totalTime}ms`);
415
+ console.log('[ImageCompression] ═══════════════════════════════════════');
416
+ return compressedFile;
417
+ }
418
+ }
419
+ catch (error) {
420
+ console.error(`[ImageCompression] ✗ Attempt ${attemptCount} failed at quality ${quality * 100}%:`, error);
421
+ }
422
+ }
423
+ // If we've exhausted all quality levels, return the best we could achieve
424
+ const totalTime = (performance.now() - startTime).toFixed(2);
425
+ const finalReduction = ((1 - compressedFile.size / originalSizeBytes) * 100).toFixed(1);
426
+ console.log('[ImageCompression] ═══════════════════════════════════════');
427
+ console.warn('[ImageCompression] ⚠ COMPRESSION LIMIT REACHED');
428
+ console.log(`[ImageCompression] Original size: ${this.formatFileSize(originalSizeBytes)}`);
429
+ console.log(`[ImageCompression] Final size: ${this.formatFileSize(compressedFile.size)}`);
430
+ console.log(`[ImageCompression] Size reduction: ${finalReduction}%`);
431
+ console.log(`[ImageCompression] Target was: ${MAX_COMPRESSED_FILE_SIZE_MB}MB`);
432
+ console.log(`[ImageCompression] Attempts: ${attemptCount}`);
433
+ console.log(`[ImageCompression] Total time: ${totalTime}ms`);
434
+ console.warn(`[ImageCompression] Could not compress below ${MAX_COMPRESSED_FILE_SIZE_MB}MB, returning best result`);
435
+ console.log('[ImageCompression] ═══════════════════════════════════════');
436
+ return compressedFile;
437
+ }
438
+ /**
439
+ * Formats file size in human-readable format
440
+ */
441
+ formatFileSize(bytes) {
442
+ if (bytes < 1024) {
443
+ return `${bytes} B`;
444
+ }
445
+ else if (bytes < 1024 * 1024) {
446
+ return `${(bytes / 1024).toFixed(2)} KB`;
447
+ }
448
+ else {
449
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
450
+ }
451
+ }
452
+ /**
453
+ * Checks if the given MIME type is a compressible image format
454
+ */
455
+ isCompressibleImage(mimeType) {
456
+ const compressibleTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/bmp'];
457
+ return compressibleTypes.includes(mimeType.toLowerCase());
458
+ }
459
+ /**
460
+ * Gets file extension from MIME type
461
+ */
462
+ getExtensionFromMimeType(mimeType) {
463
+ const mimeToExt = {
464
+ 'image/jpeg': 'jpg',
465
+ 'image/jpg': 'jpg',
466
+ 'image/png': 'png',
467
+ 'image/webp': 'webp',
468
+ 'image/gif': 'gif',
469
+ 'image/bmp': 'bmp',
470
+ 'image/svg+xml': 'svg',
471
+ };
472
+ return mimeToExt[mimeType.toLowerCase()] || 'bin';
473
+ }
474
+ dataURItoBlob(dataURI, mimeType) {
475
+ const byteString = window.atob(dataURI);
476
+ const arrayBuffer = new ArrayBuffer(byteString.length);
477
+ const int8Array = new Uint8Array(arrayBuffer);
478
+ for (let i = 0; i < byteString.length; i++) {
479
+ int8Array[i] = byteString.charCodeAt(i);
480
+ }
481
+ const blob = new Blob([int8Array], { type: mimeType });
482
+ return blob;
483
+ }
484
+ getDataUrlFromFile(file) {
485
+ return new Promise((resolve, reject) => {
486
+ var reader = new FileReader();
487
+ reader.onload = (e) => {
488
+ resolve(e.target.result);
489
+ };
490
+ reader.onerror = (e) => {
491
+ reject(e.target.error);
492
+ };
493
+ reader.readAsDataURL(file);
494
+ });
495
+ }
496
+ getUploadUrl(url, filename, mimeType, type) {
497
+ return this.http.post(url, { filename, mimeType, type });
498
+ }
499
+ attachFilesToEntity(url, data, limit) {
500
+ return this.http.post(`${url}?limit=${limit}`, data);
501
+ }
502
+ updateTag(url, data) {
503
+ return this.http.post(url, data);
504
+ }
505
+ updateSequence(url, data) {
506
+ return this.http.post(url, data);
507
+ }
508
+ putFile(url, file, contentType) {
509
+ const resolvedContentType = contentType || (file instanceof Blob ? file.type : '') || 'binary/octet-stream';
510
+ const byteLength = file instanceof ArrayBuffer
511
+ ? file.byteLength
512
+ : file.size;
513
+ const fileName = file instanceof File ? file.name : '(blob/arraybuffer)';
514
+ // Debug: inspect payload before AWS PUT (do not put file bytes in headers —
515
+ // S3 signed URLs reject unsigned headers, and headers can't carry binary bodies).
516
+ console.log('[putFile] Uploading to AWS', {
517
+ url,
518
+ fileName,
519
+ contentType: resolvedContentType,
520
+ byteLength,
521
+ bodyType: file?.constructor?.name,
522
+ isEmpty: byteLength === 0,
523
+ });
524
+ const headers = new HttpHeaders({
525
+ 'content-type': resolvedContentType,
526
+ }).set('X-Skip-Auth-Interceptor', '');
527
+ return this.http.put(url, file, { headers });
528
+ }
529
+ deleteUploadedFile(url, data) {
530
+ return this.http.post(url, data);
531
+ }
532
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularHelperService, deps: [{ token: i1$3.MatSnackBar }, { token: i1$2.MatDialog }, { token: i3$3.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
533
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularHelperService, providedIn: 'root' });
534
+ }
535
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularHelperService, decorators: [{
536
+ type: Injectable,
537
+ args: [{
538
+ providedIn: 'root'
539
+ }]
540
+ }], ctorParameters: () => [{ type: i1$3.MatSnackBar }, { type: i1$2.MatDialog }, { type: i3$3.HttpClient }] });
541
+
542
+ class AngularConfirmationDialogComponent {
543
+ dialogRef;
544
+ data;
545
+ constructor(dialogRef, data) {
546
+ this.dialogRef = dialogRef;
547
+ this.data = data;
548
+ this.dialogRef.addPanelClass(['md-angular-w-400-px']);
549
+ }
550
+ onClose(status) {
551
+ this.dialogRef.close(status);
552
+ }
553
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularConfirmationDialogComponent, deps: [{ token: i1$2.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
554
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: AngularConfirmationDialogComponent, isStandalone: false, selector: "angular-confirmation-dialog", ngImport: i0, template: "<h1 mat-dialog-title style=\"align-items: center; display: flex;\">\n <mat-icon class=\"{{data?.iconClass}}\" style=\"color: var(--angular-cancel, #bb333b)\">{{data?.icon}}</mat-icon>\n <span>{{data?.title}}</span>\n</h1>\n<div mat-dialog-content style=\"padding: 20px 24px; padding-top: 0px;\">\n @if(data?.html){\n <h5 class=\"title\">{{data?.message}}</h5>\n <div [innerHTML]=\"data.html\">\n </div>\n }\n @else {\n <span>{{data?.message}}</span>\n }\n</div>\n<div mat-dialog-actions>\n <button mat-button class=\"{{data?.cancelButtonClass}}\" style=\"color: var(--angular-primary, var(--mat-sys-primary))\" (click)=\"onClose(false)\" *ngIf=\"data?.cancelButtonText && data?.cancelButtonText != ''\">{{data?.cancelButtonText}}</button>\n <button mat-button class=\"{{data?.approveButtonClass}}\" style=\"color: var(--angular-cancel, #bb333b)\" (click)=\"onClose(true)\" cdkFocusInitial *ngIf=\"data?.approveButtonText && data?.approveButtonText != ''\">{{data?.approveButtonText}}</button>\n</div>", styles: [""], dependencies: [{ kind: "directive", type: i3$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i4$1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "directive", type: i1$2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1$2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1$2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }] });
555
+ }
556
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularConfirmationDialogComponent, decorators: [{
557
+ type: Component,
558
+ args: [{ selector: 'angular-confirmation-dialog', standalone: false, template: "<h1 mat-dialog-title style=\"align-items: center; display: flex;\">\n <mat-icon class=\"{{data?.iconClass}}\" style=\"color: var(--angular-cancel, #bb333b)\">{{data?.icon}}</mat-icon>\n <span>{{data?.title}}</span>\n</h1>\n<div mat-dialog-content style=\"padding: 20px 24px; padding-top: 0px;\">\n @if(data?.html){\n <h5 class=\"title\">{{data?.message}}</h5>\n <div [innerHTML]=\"data.html\">\n </div>\n }\n @else {\n <span>{{data?.message}}</span>\n }\n</div>\n<div mat-dialog-actions>\n <button mat-button class=\"{{data?.cancelButtonClass}}\" style=\"color: var(--angular-primary, var(--mat-sys-primary))\" (click)=\"onClose(false)\" *ngIf=\"data?.cancelButtonText && data?.cancelButtonText != ''\">{{data?.cancelButtonText}}</button>\n <button mat-button class=\"{{data?.approveButtonClass}}\" style=\"color: var(--angular-cancel, #bb333b)\" (click)=\"onClose(true)\" cdkFocusInitial *ngIf=\"data?.approveButtonText && data?.approveButtonText != ''\">{{data?.approveButtonText}}</button>\n</div>" }]
559
+ }], ctorParameters: () => [{ type: i1$2.MatDialogRef }, { type: undefined, decorators: [{
560
+ type: Inject,
561
+ args: [MAT_DIALOG_DATA]
562
+ }] }] });
563
+
564
+ const DEFAULT_PAIRING_TTL = 24 * 60 * 60 * 1000; // 24 hours
565
+ const DEFAULT_STORAGE_KEY = 'angular-remote-upload-session';
566
+ const DEFAULT_QR_EXPIRY = 300; // 5 minutes
567
+ const HEALTH_CHECK_INTERVAL = 30000; // 30 seconds
568
+ class AngularRemoteUploadService {
569
+ http;
570
+ static COMPONENT = 'AngularRemoteUploadService';
571
+ static MOBILE_CONNECTION_KEY = 'angular-mobile-connection';
572
+ // ponytail: read/migrate old localStorage key; remove after all clients refresh
573
+ static LEGACY_MOBILE_CONNECTION_KEY = 'tis-mobile-connection';
574
+ destroy$ = new Subject();
575
+ channelSubscription = null;
576
+ healthCheckSubscription = null;
577
+ config = null;
578
+ socketAdapter = null;
579
+ // Cached values
580
+ deviceId = '';
581
+ userId = '';
582
+ apiUrl = '';
583
+ channelName = '';
584
+ // State observables
585
+ pairingSession$ = new BehaviorSubject(null);
586
+ connectionStatus$ = new BehaviorSubject('disconnected');
587
+ mobileConnection$ = new BehaviorSubject(null);
588
+ remoteUpload$ = new Subject();
589
+ error$ = new Subject();
590
+ // Pending files from mobile - waiting for user to accept/reject
591
+ pendingFiles$ = new BehaviorSubject([]);
592
+ // Flag to track if we're waiting for mobile upload
593
+ isWaitingForUpload$ = new BehaviorSubject(false);
594
+ // Current field request info
595
+ currentFieldRequest$ = new BehaviorSubject(null);
596
+ // Device online status - 'checking' means initial check in progress (blinking dot)
597
+ devicesStatus$ = new BehaviorSubject(null);
598
+ isCheckingStatus$ = new BehaviorSubject(false);
599
+ constructor(http) {
600
+ this.http = http;
601
+ // Restore mobile connection from storage on init
602
+ this.restoreMobileConnection();
603
+ }
604
+ // ===========================================================================
605
+ // Configuration
606
+ // ===========================================================================
607
+ /**
608
+ * Configure the remote upload service
609
+ */
610
+ async configure(config) {
611
+ this.config = config;
612
+ this.socketAdapter = config.socketAdapter || null;
613
+ if (config.enabled && this.socketAdapter) {
614
+ // Get device ID, user ID, and API URL from adapter
615
+ this.deviceId = await Promise.resolve(this.socketAdapter.getDeviceId());
616
+ this.userId = this.socketAdapter.getUserId
617
+ ? await Promise.resolve(this.socketAdapter.getUserId())
618
+ : '';
619
+ this.apiUrl = this.socketAdapter.getApiUrl?.() || '';
620
+ this.channelName = `tis-mobile-upload-w-dev-${this.deviceId}`;
621
+ // Subscribe to socket connection status
622
+ this.socketAdapter.connectionStatus$
623
+ .pipe(takeUntil(this.destroy$))
624
+ .subscribe(connected => {
625
+ if (connected) {
626
+ // Always subscribe to our channel when connected
627
+ this.subscribeToChannel(this.channelName);
628
+ // If we have a restored connection, start health check
629
+ if (this.isConnectedToMobile()) {
630
+ this.startHealthCheck();
631
+ }
632
+ }
633
+ });
634
+ // Subscribe to channel immediately if already connected
635
+ if (this.socketAdapter.isConnected()) {
636
+ this.subscribeToChannel(this.channelName);
637
+ // If we have a restored connection, start health check
638
+ if (this.isConnectedToMobile()) {
639
+ this.startHealthCheck();
640
+ }
641
+ }
642
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Configured:`, {
643
+ deviceId: this.deviceId,
644
+ userId: this.userId,
645
+ channel: this.channelName
646
+ });
647
+ }
648
+ }
649
+ // ===========================================================================
650
+ // Public Getters
651
+ // ===========================================================================
652
+ /**
653
+ * Get desktop device ID
654
+ */
655
+ getDesktopDeviceId() {
656
+ return this.deviceId;
657
+ }
658
+ /**
659
+ * Get current mobile connection info
660
+ */
661
+ getMobileConnection() {
662
+ return this.mobileConnection$.asObservable();
663
+ }
664
+ /**
665
+ * Get current mobile device ID (if connected)
666
+ */
667
+ getMobileDeviceId() {
668
+ return this.mobileConnection$.value?.mobileDeviceId || null;
669
+ }
670
+ /**
671
+ * Get current pairing session
672
+ */
673
+ getPairingSession() {
674
+ return this.pairingSession$.asObservable();
675
+ }
676
+ /**
677
+ * Get connection status
678
+ */
679
+ getConnectionStatus() {
680
+ return this.connectionStatus$.asObservable();
681
+ }
682
+ /**
683
+ * Get remote upload events
684
+ */
685
+ getRemoteUploads() {
686
+ return this.remoteUpload$.asObservable();
687
+ }
688
+ /**
689
+ * Get error events
690
+ */
691
+ getErrors() {
692
+ return this.error$.asObservable();
693
+ }
694
+ /**
695
+ * Check if remote upload is available (configured and socket connected)
696
+ */
697
+ isAvailable() {
698
+ return !!(this.config?.enabled &&
699
+ this.socketAdapter &&
700
+ this.socketAdapter.isConnected());
701
+ }
702
+ /**
703
+ * Check if currently connected to a mobile device
704
+ */
705
+ isConnectedToMobile() {
706
+ return this.connectionStatus$.value === 'connected' && !!this.mobileConnection$.value;
707
+ }
708
+ /**
709
+ * Alias for isConnectedToMobile - Check if paired with mobile
710
+ */
711
+ isPaired() {
712
+ return this.isConnectedToMobile();
713
+ }
714
+ /**
715
+ * Get devices online status (both desktop and mobile)
716
+ */
717
+ getDevicesStatus() {
718
+ return this.devicesStatus$.asObservable();
719
+ }
720
+ /**
721
+ * Get current devices status value
722
+ */
723
+ getDevicesStatusValue() {
724
+ return this.devicesStatus$.value;
725
+ }
726
+ /**
727
+ * Check if currently verifying connection status (for blinking indicator)
728
+ */
729
+ getIsCheckingStatus() {
730
+ return this.isCheckingStatus$.asObservable();
731
+ }
732
+ /**
733
+ * Check if both devices are online and ready for transfer
734
+ */
735
+ isReadyForTransfer() {
736
+ return this.devicesStatus$.value?.isReadyForTransfer ?? false;
737
+ }
738
+ /**
739
+ * Get pending files from mobile (waiting to be accepted/rejected)
740
+ */
741
+ getPendingFiles() {
742
+ return this.pendingFiles$.asObservable();
743
+ }
744
+ /**
745
+ * Get current pending files value
746
+ */
747
+ getPendingFilesValue() {
748
+ return this.pendingFiles$.value;
749
+ }
750
+ /**
751
+ * Check if waiting for mobile upload
752
+ */
753
+ getIsWaitingForUpload() {
754
+ return this.isWaitingForUpload$.asObservable();
755
+ }
756
+ /**
757
+ * Get current field request info
758
+ */
759
+ getCurrentFieldRequest() {
760
+ return this.currentFieldRequest$.asObservable();
761
+ }
762
+ /**
763
+ * Accept a pending file (emit to remoteUpload$ and call onFileAccept callback)
764
+ */
765
+ acceptPendingFile(file) {
766
+ // Remove from pending
767
+ const currentPending = this.pendingFiles$.value;
768
+ this.pendingFiles$.next(currentPending.filter(f => f !== file));
769
+ // Emit to remoteUpload$ so component can add it
770
+ this.remoteUpload$.next(file);
771
+ // Call onFileAccept callback if provided
772
+ if (this.config?.onFileAccept) {
773
+ this.config.onFileAccept(file.file);
774
+ }
775
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] File accepted:`, file.file.fileName);
776
+ }
777
+ /**
778
+ * Reject/delete a pending file
779
+ */
780
+ rejectPendingFile(file) {
781
+ const currentPending = this.pendingFiles$.value;
782
+ this.pendingFiles$.next(currentPending.filter(f => f !== file));
783
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] File rejected:`, file.file.fileName);
784
+ }
785
+ /**
786
+ * Clear all pending files
787
+ */
788
+ clearPendingFiles() {
789
+ this.pendingFiles$.next([]);
790
+ }
791
+ // ===========================================================================
792
+ // QR Code Generation (New Flow)
793
+ // ===========================================================================
794
+ /**
795
+ * Generate QR code data for mobile app
796
+ * Flow:
797
+ * 1. Call API to get a short-lived link token (UUID)
798
+ * 2. Build QR URL with: apiUrl, deviceId, userId, token
799
+ */
800
+ async generateQrCode() {
801
+ if (!this.isAvailable()) {
802
+ throw new Error('Remote upload is not available. Check configuration and socket connection.');
803
+ }
804
+ if (!this.apiUrl) {
805
+ throw new Error('API URL not configured in socket adapter');
806
+ }
807
+ try {
808
+ // Step 1: Get link token from backend
809
+ const endpoint = this.config?.apiEndpoints?.generateMobileLinkToken
810
+ || `${this.apiUrl}/ease-of-access/mobile-upload-link-token`;
811
+ const apiResponse = await this.callHttpApi(endpoint, { deviceId: this.deviceId, userId: this.userId });
812
+ // Unwrap the data from API response
813
+ const response = apiResponse.data || apiResponse;
814
+ const expirySeconds = this.config?.qrCode?.expirySeconds || DEFAULT_QR_EXPIRY;
815
+ const expiresAt = response.expiresAt || Date.now() + expirySeconds * 1000;
816
+ // Step 2: Build QR URL
817
+ const mobileUrl = this.config?.qrCode?.mobileUploadUrl || '';
818
+ const qrData = this.buildQrUrl(mobileUrl, {
819
+ token: response.token,
820
+ deviceId: this.deviceId,
821
+ userId: this.userId,
822
+ apiUrl: this.apiUrl
823
+ });
824
+ // Update pairing session
825
+ const session = {
826
+ pairingCode: response.token,
827
+ desktopDeviceId: this.deviceId,
828
+ channel: this.channelName,
829
+ createdAt: Date.now(),
830
+ expiresAt,
831
+ status: 'pending'
832
+ };
833
+ this.pairingSession$.next(session);
834
+ this.connectionStatus$.next('pending');
835
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] QR code generated, waiting for mobile...`);
836
+ return { qrData, expiresAt };
837
+ }
838
+ catch (error) {
839
+ const msg = `Failed to generate QR code: ${error.message}`;
840
+ console.error(`[${AngularRemoteUploadService.COMPONENT}]`, msg);
841
+ this.error$.next(msg);
842
+ throw error;
843
+ }
844
+ }
845
+ /**
846
+ * Build QR URL with minimal parameters
847
+ */
848
+ buildQrUrl(baseUrl, params) {
849
+ const url = new URL(baseUrl);
850
+ url.searchParams.set('token', params.token);
851
+ url.searchParams.set('deviceId', params.deviceId);
852
+ url.searchParams.set('userId', params.userId);
853
+ url.searchParams.set('apiUrl', encodeURIComponent(params.apiUrl));
854
+ return url.toString();
855
+ }
856
+ // ===========================================================================
857
+ // Mobile Communication (via API calls)
858
+ // ===========================================================================
859
+ /**
860
+ * Call API via socket with timeout - helper method
861
+ */
862
+ callApiWithTimeout(route, body, timeoutMs = 10000) {
863
+ return new Promise((resolve, reject) => {
864
+ if (!this.socketAdapter?.callApiViaSocket) {
865
+ reject(new Error('callApiViaSocket not available'));
866
+ return;
867
+ }
868
+ const timeout = setTimeout(() => reject(new Error(`API call timeout: ${route}`)), timeoutMs);
869
+ const callApi = this.socketAdapter.callApiViaSocket.bind(this.socketAdapter);
870
+ callApi(route, body).pipe(take(1)).subscribe({
871
+ next: (res) => {
872
+ clearTimeout(timeout);
873
+ resolve(res);
874
+ },
875
+ error: (err) => {
876
+ clearTimeout(timeout);
877
+ reject(err);
878
+ }
879
+ });
880
+ });
881
+ }
882
+ /**
883
+ * Send field request to mobile - tells mobile to show upload UI for this field
884
+ */
885
+ async sendFieldRequest(fieldInfo) {
886
+ if (!this.isConnectedToMobile()) {
887
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Not connected to mobile, cannot send field request`);
888
+ return;
889
+ }
890
+ const mobileDeviceId = this.mobileConnection$.value?.mobileDeviceId;
891
+ const requestId = `field-${Date.now()}`;
892
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Sending field request to mobile:`, fieldInfo);
893
+ // Set waiting state and store field request info
894
+ this.isWaitingForUpload$.next(true);
895
+ this.currentFieldRequest$.next({
896
+ ...fieldInfo,
897
+ requestId,
898
+ requestedAt: Date.now()
899
+ });
900
+ try {
901
+ const response = await this.callApiWithTimeout('tis-image-mobile-uploader/field-request', {
902
+ desktopDeviceId: this.deviceId,
903
+ mobileDeviceId: mobileDeviceId,
904
+ channel: this.channelName,
905
+ field: fieldInfo,
906
+ requestId
907
+ });
908
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Field request sent:`, response);
909
+ }
910
+ catch (error) {
911
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Field request failed:`, error);
912
+ // Reset waiting state on error
913
+ this.isWaitingForUpload$.next(false);
914
+ this.currentFieldRequest$.next(null);
915
+ }
916
+ }
917
+ /**
918
+ * Cancel current field request
919
+ */
920
+ async cancelFieldRequest() {
921
+ // Reset waiting state
922
+ this.isWaitingForUpload$.next(false);
923
+ this.currentFieldRequest$.next(null);
924
+ if (!this.isConnectedToMobile()) {
925
+ return;
926
+ }
927
+ const mobileDeviceId = this.mobileConnection$.value?.mobileDeviceId;
928
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Canceling field request`);
929
+ try {
930
+ await this.callApiWithTimeout('tis-image-mobile-uploader/cancel-field-request', {
931
+ desktopDeviceId: this.deviceId,
932
+ mobileDeviceId: mobileDeviceId,
933
+ channel: this.channelName
934
+ });
935
+ }
936
+ catch (error) {
937
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Cancel field request failed:`, error);
938
+ }
939
+ }
940
+ /**
941
+ * Accept mobile connection - update local state
942
+ * Note: Backend already broadcasts mobile-link-established to both parties
943
+ */
944
+ acceptMobileConnection(mobileDeviceId) {
945
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Accepting mobile connection:`, mobileDeviceId);
946
+ // Save mobile connection
947
+ const connectionInfo = {
948
+ mobileDeviceId,
949
+ connectedAt: Date.now(),
950
+ lastActivity: Date.now()
951
+ };
952
+ this.mobileConnection$.next(connectionInfo);
953
+ this.saveMobileConnection(connectionInfo);
954
+ // Update status
955
+ this.connectionStatus$.next('connected');
956
+ // Update session
957
+ const session = this.pairingSession$.value;
958
+ if (session) {
959
+ const updatedSession = {
960
+ ...session,
961
+ mobileDeviceId,
962
+ status: 'connected',
963
+ lastActivity: Date.now()
964
+ };
965
+ this.pairingSession$.next(updatedSession);
966
+ }
967
+ }
968
+ /**
969
+ * Disconnect from mobile device - call API and clear local state
970
+ */
971
+ async disconnect() {
972
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Disconnecting from mobile...`);
973
+ const mobileDeviceId = this.mobileConnection$.value?.mobileDeviceId;
974
+ // Call disconnect API via socket
975
+ if (this.socketAdapter?.callApiViaSocket && mobileDeviceId) {
976
+ try {
977
+ const callApi = this.socketAdapter.callApiViaSocket.bind(this.socketAdapter);
978
+ const response = await new Promise((resolve, reject) => {
979
+ const timeout = setTimeout(() => reject(new Error('Disconnect API timeout')), 10000);
980
+ callApi('tis-image-mobile-uploader/disconnect-mobile-link', {
981
+ desktopDeviceId: this.deviceId,
982
+ mobileDeviceId: mobileDeviceId,
983
+ initiatedBy: 'desktop'
984
+ }).pipe(take(1)).subscribe({
985
+ next: (res) => {
986
+ clearTimeout(timeout);
987
+ resolve(res);
988
+ },
989
+ error: (err) => {
990
+ clearTimeout(timeout);
991
+ reject(err);
992
+ }
993
+ });
994
+ });
995
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Disconnect API response:`, response);
996
+ }
997
+ catch (error) {
998
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Disconnect API call failed:`, error);
999
+ // Continue with local cleanup anyway
1000
+ }
1001
+ }
1002
+ // Clear state
1003
+ this.mobileConnection$.next(null);
1004
+ this.connectionStatus$.next('disconnected');
1005
+ this.pairingSession$.next(null);
1006
+ this.clearMobileConnection();
1007
+ // Stop health check
1008
+ this.stopHealthCheck();
1009
+ }
1010
+ /**
1011
+ * Handle disconnect initiated from remote (mobile) side
1012
+ */
1013
+ handleRemoteDisconnect(data) {
1014
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Mobile disconnected:`, data);
1015
+ // Clear state without calling API (mobile already initiated)
1016
+ this.mobileConnection$.next(null);
1017
+ this.connectionStatus$.next('disconnected');
1018
+ this.pairingSession$.next(null);
1019
+ this.clearMobileConnection();
1020
+ // Stop health check when disconnected
1021
+ this.stopHealthCheck();
1022
+ }
1023
+ // ===========================================================================
1024
+ // Device Health Check
1025
+ // ===========================================================================
1026
+ /**
1027
+ * Start periodic health check for device online status
1028
+ * Should be called when connection is established
1029
+ */
1030
+ startHealthCheck() {
1031
+ // Stop any existing health check
1032
+ this.stopHealthCheck();
1033
+ const mobileDeviceId = this.mobileConnection$.value?.mobileDeviceId;
1034
+ if (!mobileDeviceId || !this.deviceId) {
1035
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Cannot start health check - missing device IDs`, {
1036
+ mobileDeviceId,
1037
+ desktopDeviceId: this.deviceId
1038
+ });
1039
+ return;
1040
+ }
1041
+ if (!this.socketAdapter?.callApiViaSocket) {
1042
+ console.error(`[${AngularRemoteUploadService.COMPONENT}] Cannot start health check - socketAdapter.callApiViaSocket not available`);
1043
+ return;
1044
+ }
1045
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Starting health check (every ${HEALTH_CHECK_INTERVAL / 1000}s)`, {
1046
+ mobileDeviceId,
1047
+ desktopDeviceId: this.deviceId
1048
+ });
1049
+ // Set initial checking state (blinking)
1050
+ this.isCheckingStatus$.next(true);
1051
+ // Run first check immediately
1052
+ this.checkDevicesOnline().catch(err => {
1053
+ console.error(`[${AngularRemoteUploadService.COMPONENT}] Initial health check failed:`, err);
1054
+ });
1055
+ // Then run periodically
1056
+ this.healthCheckSubscription = interval(HEALTH_CHECK_INTERVAL)
1057
+ .pipe(takeUntil(this.destroy$))
1058
+ .subscribe(() => {
1059
+ this.checkDevicesOnline().catch(err => {
1060
+ console.error(`[${AngularRemoteUploadService.COMPONENT}] Periodic health check failed:`, err);
1061
+ });
1062
+ });
1063
+ }
1064
+ /**
1065
+ * Stop health check
1066
+ */
1067
+ stopHealthCheck() {
1068
+ if (this.healthCheckSubscription) {
1069
+ this.healthCheckSubscription.unsubscribe();
1070
+ this.healthCheckSubscription = null;
1071
+ }
1072
+ this.devicesStatus$.next(null);
1073
+ this.isCheckingStatus$.next(false);
1074
+ }
1075
+ /**
1076
+ * Check if both devices are online via API
1077
+ */
1078
+ async checkDevicesOnline() {
1079
+ const mobileDeviceId = this.mobileConnection$.value?.mobileDeviceId;
1080
+ if (!mobileDeviceId || !this.deviceId) {
1081
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Cannot check devices - missing device IDs`, {
1082
+ mobileDeviceId,
1083
+ desktopDeviceId: this.deviceId
1084
+ });
1085
+ return null;
1086
+ }
1087
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Checking devices online status...`, {
1088
+ desktopDeviceId: this.deviceId,
1089
+ mobileDeviceId: mobileDeviceId
1090
+ });
1091
+ try {
1092
+ const response = await this.callApiWithTimeout('tis-image-mobile-uploader/check-devices-online', {
1093
+ desktopDeviceId: this.deviceId,
1094
+ mobileDeviceId: mobileDeviceId
1095
+ }, 15000);
1096
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Check devices response:`, response);
1097
+ // Response structure: { body: { data: { desktop: {...}, mobile: {...} } } }
1098
+ const data = response?.body?.data || response?.data || response;
1099
+ const status = {
1100
+ desktop: {
1101
+ isOnline: data.desktop?.isOnline ?? false,
1102
+ deviceId: data.desktop?.deviceId || this.deviceId,
1103
+ lastPing: data.desktop?.lastPing,
1104
+ connectionId: data.desktop?.connectionId
1105
+ },
1106
+ mobile: {
1107
+ isOnline: data.mobile?.isOnline ?? false,
1108
+ deviceId: data.mobile?.deviceId || mobileDeviceId,
1109
+ lastPing: data.mobile?.lastPing,
1110
+ connectionId: data.mobile?.connectionId
1111
+ },
1112
+ lastChecked: Date.now(),
1113
+ isReadyForTransfer: (data.desktop?.isOnline ?? false) && (data.mobile?.isOnline ?? false)
1114
+ };
1115
+ this.devicesStatus$.next(status);
1116
+ this.isCheckingStatus$.next(false); // Stop blinking after first successful check
1117
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Devices status:`, {
1118
+ desktop: status.desktop.isOnline ? '🟢' : '🔴',
1119
+ mobile: status.mobile.isOnline ? '🟢' : '🔴',
1120
+ readyForTransfer: status.isReadyForTransfer
1121
+ });
1122
+ // If desktop device (this device) is offline, send immediate ping to update status
1123
+ if (!status.desktop.isOnline && this.socketAdapter) {
1124
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Desktop detected as offline, sending immediate ping...`);
1125
+ this.callApiWithTimeout('ping', { timestamp: Date.now() }, 5000)
1126
+ .then(() => {
1127
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Immediate ping sent, rechecking status...`);
1128
+ // Recheck status after a short delay
1129
+ setTimeout(() => this.checkDevicesOnline(), 1000);
1130
+ })
1131
+ .catch((err) => {
1132
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Immediate ping failed:`, err);
1133
+ });
1134
+ }
1135
+ return status;
1136
+ }
1137
+ catch (error) {
1138
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Health check failed:`, error.message);
1139
+ // On error, mark both as unknown/offline
1140
+ const status = {
1141
+ desktop: { isOnline: false, deviceId: this.deviceId },
1142
+ mobile: { isOnline: false, deviceId: mobileDeviceId },
1143
+ lastChecked: Date.now(),
1144
+ isReadyForTransfer: false
1145
+ };
1146
+ this.devicesStatus$.next(status);
1147
+ this.isCheckingStatus$.next(false);
1148
+ return status;
1149
+ }
1150
+ }
1151
+ /**
1152
+ * Force an immediate health check
1153
+ */
1154
+ async refreshDevicesStatus() {
1155
+ this.isCheckingStatus$.next(true);
1156
+ return this.checkDevicesOnline();
1157
+ }
1158
+ // ===========================================================================
1159
+ // Channel Subscription & Message Handling
1160
+ // ===========================================================================
1161
+ /**
1162
+ * Subscribe to channel for receiving messages from mobile
1163
+ */
1164
+ subscribeToChannel(channel) {
1165
+ if (this.channelSubscription) {
1166
+ this.channelSubscription.unsubscribe();
1167
+ }
1168
+ if (!this.socketAdapter) {
1169
+ return;
1170
+ }
1171
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Subscribing to channel:`, channel);
1172
+ this.channelSubscription = this.socketAdapter.subscribeToChannel(channel)
1173
+ .pipe(takeUntil(this.destroy$))
1174
+ .subscribe({
1175
+ next: (message) => this.handleChannelMessage(message),
1176
+ error: (error) => {
1177
+ console.error(`[${AngularRemoteUploadService.COMPONENT}] Channel error:`, error);
1178
+ this.error$.next(`Channel subscription error: ${error.message}`);
1179
+ }
1180
+ });
1181
+ }
1182
+ /**
1183
+ * Handle incoming channel messages from mobile
1184
+ */
1185
+ handleChannelMessage(message) {
1186
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Received:`, message);
1187
+ // Extract message type and data - handle nested payload structure
1188
+ const payload = message.payload || message.data || message;
1189
+ const type = payload.type || message.type;
1190
+ const data = payload;
1191
+ switch (type) {
1192
+ case 'connectionState':
1193
+ this.handleConnectionState(data);
1194
+ break;
1195
+ case 'mobile-link-established':
1196
+ this.handleMobileLinkEstablished(data);
1197
+ break;
1198
+ case 'file-uploaded':
1199
+ case 'image-uploaded':
1200
+ case 'upload_complete':
1201
+ this.handleUploadComplete(message);
1202
+ break;
1203
+ case 'disconnect':
1204
+ case 'mobile-link-disconnected':
1205
+ this.handleMobileDisconnect(data);
1206
+ break;
1207
+ default:
1208
+ // Check if it's an upload event
1209
+ if (data.file || data.fileName || data.fileUrl) {
1210
+ this.handleUploadComplete(message);
1211
+ }
1212
+ else {
1213
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Unknown message type:`, type);
1214
+ }
1215
+ }
1216
+ }
1217
+ /**
1218
+ * Handle mobile-link-established message
1219
+ * This is sent when mobile successfully connects via the backend
1220
+ */
1221
+ handleMobileLinkEstablished(data) {
1222
+ const mobileDeviceId = data.mobileDeviceId;
1223
+ const mobileConnectionId = data.mobileConnectionId;
1224
+ const userId = data.userId;
1225
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Mobile link established:`, {
1226
+ mobileDeviceId,
1227
+ mobileConnectionId,
1228
+ userId
1229
+ });
1230
+ if (mobileDeviceId) {
1231
+ // Save mobile connection
1232
+ const connectionInfo = {
1233
+ mobileDeviceId,
1234
+ connectedAt: Date.now(),
1235
+ lastActivity: Date.now()
1236
+ };
1237
+ this.mobileConnection$.next(connectionInfo);
1238
+ this.saveMobileConnection(connectionInfo);
1239
+ // Update status to connected
1240
+ this.connectionStatus$.next('connected');
1241
+ // Update session
1242
+ const session = this.pairingSession$.value;
1243
+ if (session) {
1244
+ const updatedSession = {
1245
+ ...session,
1246
+ mobileDeviceId,
1247
+ status: 'connected',
1248
+ lastActivity: Date.now()
1249
+ };
1250
+ this.pairingSession$.next(updatedSession);
1251
+ }
1252
+ // Start health check to monitor device online status
1253
+ this.startHealthCheck();
1254
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Connection established with mobile device:`, mobileDeviceId);
1255
+ }
1256
+ }
1257
+ /**
1258
+ * Handle connection state messages from mobile
1259
+ */
1260
+ handleConnectionState(data) {
1261
+ const state = data.state || data.connectionState;
1262
+ const mobileDeviceId = data.mobileDeviceId;
1263
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Connection state:`, state, 'from:', mobileDeviceId);
1264
+ if (state === 'INITIATED' && mobileDeviceId) {
1265
+ // Mobile is initiating connection - accept it
1266
+ this.acceptMobileConnection(mobileDeviceId);
1267
+ }
1268
+ }
1269
+ /**
1270
+ * Handle upload complete from mobile
1271
+ * Files are either auto-accepted or added to pending queue based on config
1272
+ */
1273
+ handleUploadComplete(message) {
1274
+ const data = message.data || message.payload || message;
1275
+ // Handle both single file and files array
1276
+ const files = data.files || (data.file ? [data.file] : [data]);
1277
+ for (const file of files) {
1278
+ if (file && (file.s3Url || file.s3Path || file.resourceUrl)) {
1279
+ // Normalize the file object
1280
+ const normalizedFile = {
1281
+ s3Url: file.s3Url || file.resourceUrl || file.uploadData?.resourceUrl || '',
1282
+ fileName: file.fileName || file.filename || file.name || file.title || file.uploadData?.fileName || file.uploadData?.photoFilename || 'unknown',
1283
+ mimeType: file.mimeType || file.type || 'application/octet-stream',
1284
+ size: file.size || 0,
1285
+ thumbnailUrl: file.thumbnailUrl,
1286
+ metadata: file.metadata,
1287
+ uploadData: file.uploadData
1288
+ };
1289
+ const event = {
1290
+ file: normalizedFile,
1291
+ mobileDeviceId: data.mobileDeviceId || this.mobileConnection$.value?.mobileDeviceId || 'unknown',
1292
+ timestamp: data.timestamp || Date.now(),
1293
+ sessionId: data.sessionId
1294
+ };
1295
+ // Check if auto-accept is enabled
1296
+ if (this.config?.autoAcceptFiles) {
1297
+ // Auto-accept: emit to remoteUpload$ (component will show in UI and attach to form)
1298
+ this.remoteUpload$.next(event);
1299
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] File auto-accepted (showing in UI):`, normalizedFile.fileName);
1300
+ // Call onFileAccept callback if provided
1301
+ if (this.config?.onFileAccept) {
1302
+ this.config.onFileAccept(normalizedFile);
1303
+ }
1304
+ }
1305
+ else {
1306
+ // Auto-accept OFF: Only call callback, don't show in UI or attach to form
1307
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] File received (not auto-accepting, callback only):`, normalizedFile.fileName);
1308
+ // Call onFileAccept callback if provided (host app handles display/processing)
1309
+ if (this.config?.onFileAccept) {
1310
+ this.config.onFileAccept(normalizedFile);
1311
+ }
1312
+ // Note: File is NOT added to pending queue or emitted to remoteUpload$
1313
+ // The host app's onFileAccept callback is responsible for handling the file
1314
+ }
1315
+ }
1316
+ }
1317
+ // Update last activity
1318
+ const conn = this.mobileConnection$.value;
1319
+ if (conn) {
1320
+ const updated = { ...conn, lastActivity: Date.now() };
1321
+ this.mobileConnection$.next(updated);
1322
+ this.saveMobileConnection(updated);
1323
+ }
1324
+ // Reset waiting state - files received
1325
+ this.isWaitingForUpload$.next(false);
1326
+ }
1327
+ /**
1328
+ * Handle mobile disconnect
1329
+ */
1330
+ handleMobileDisconnect(data) {
1331
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Mobile disconnected:`, data);
1332
+ this.mobileConnection$.next(null);
1333
+ this.connectionStatus$.next('disconnected');
1334
+ this.clearMobileConnection();
1335
+ // Update session
1336
+ const session = this.pairingSession$.value;
1337
+ if (session) {
1338
+ this.pairingSession$.next({
1339
+ ...session,
1340
+ status: 'disconnected',
1341
+ lastActivity: Date.now()
1342
+ });
1343
+ }
1344
+ }
1345
+ // ===========================================================================
1346
+ // Storage
1347
+ // ===========================================================================
1348
+ /**
1349
+ * Save mobile connection to localStorage
1350
+ */
1351
+ saveMobileConnection(info) {
1352
+ try {
1353
+ localStorage.setItem(AngularRemoteUploadService.MOBILE_CONNECTION_KEY, JSON.stringify(info));
1354
+ }
1355
+ catch (e) {
1356
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Failed to save connection:`, e);
1357
+ }
1358
+ }
1359
+ /**
1360
+ * Restore mobile connection from localStorage
1361
+ */
1362
+ restoreMobileConnection() {
1363
+ try {
1364
+ const stored = localStorage.getItem(AngularRemoteUploadService.MOBILE_CONNECTION_KEY) ??
1365
+ localStorage.getItem(AngularRemoteUploadService.LEGACY_MOBILE_CONNECTION_KEY);
1366
+ if (stored) {
1367
+ const info = JSON.parse(stored);
1368
+ // Only restore if connected within last 24 hours
1369
+ if (Date.now() - info.lastActivity < DEFAULT_PAIRING_TTL) {
1370
+ this.mobileConnection$.next(info);
1371
+ // Also restore connection status so UI shows connected state
1372
+ this.connectionStatus$.next('connected');
1373
+ // Migrate legacy key if present
1374
+ this.saveMobileConnection(info);
1375
+ localStorage.removeItem(AngularRemoteUploadService.LEGACY_MOBILE_CONNECTION_KEY);
1376
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Restored mobile connection from localStorage:`, info.mobileDeviceId);
1377
+ }
1378
+ else {
1379
+ console.log(`[${AngularRemoteUploadService.COMPONENT}] Stored connection expired, clearing`);
1380
+ this.clearMobileConnection();
1381
+ }
1382
+ }
1383
+ }
1384
+ catch (e) {
1385
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Failed to restore connection:`, e);
1386
+ }
1387
+ }
1388
+ /**
1389
+ * Clear mobile connection from localStorage
1390
+ */
1391
+ clearMobileConnection() {
1392
+ try {
1393
+ localStorage.removeItem(AngularRemoteUploadService.MOBILE_CONNECTION_KEY);
1394
+ localStorage.removeItem(AngularRemoteUploadService.LEGACY_MOBILE_CONNECTION_KEY);
1395
+ }
1396
+ catch (e) {
1397
+ console.warn(`[${AngularRemoteUploadService.COMPONENT}] Failed to clear connection:`, e);
1398
+ }
1399
+ }
1400
+ // ===========================================================================
1401
+ // HTTP API
1402
+ // ===========================================================================
1403
+ /**
1404
+ * Call HTTP API endpoint
1405
+ */
1406
+ callHttpApi(endpoint, data) {
1407
+ return new Promise((resolve, reject) => {
1408
+ this.http.post(endpoint, data)
1409
+ .pipe(take(1), timeout(30000))
1410
+ .subscribe({
1411
+ next: resolve,
1412
+ error: reject
1413
+ });
1414
+ });
1415
+ }
1416
+ // ===========================================================================
1417
+ // Cleanup
1418
+ // ===========================================================================
1419
+ ngOnDestroy() {
1420
+ this.destroy$.next();
1421
+ this.destroy$.complete();
1422
+ if (this.channelSubscription) {
1423
+ this.channelSubscription.unsubscribe();
1424
+ }
1425
+ if (this.healthCheckSubscription) {
1426
+ this.healthCheckSubscription.unsubscribe();
1427
+ }
1428
+ }
1429
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularRemoteUploadService, deps: [{ token: i3$3.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
1430
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularRemoteUploadService, providedIn: 'root' });
1431
+ }
1432
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularRemoteUploadService, decorators: [{
1433
+ type: Injectable,
1434
+ args: [{
1435
+ providedIn: 'root'
1436
+ }]
1437
+ }], ctorParameters: () => [{ type: i3$3.HttpClient }] });
1438
+
1439
+ class AngularQrCodeDialogComponent {
1440
+ dialogRef;
1441
+ data;
1442
+ remoteUploadService;
1443
+ qrData = '';
1444
+ expiresAt = 0;
1445
+ remainingTime = '';
1446
+ isLoading = true;
1447
+ isExpired = false;
1448
+ isConnected = false;
1449
+ errorMessage = null;
1450
+ connectionStatus = 'disconnected';
1451
+ // Device IDs
1452
+ desktopDeviceId = '';
1453
+ mobileDeviceId = null;
1454
+ // Device online status
1455
+ devicesStatus = null;
1456
+ isCheckingStatus = false; // For blinking indicator
1457
+ destroy$ = new Subject();
1458
+ countdownSubscription = null;
1459
+ constructor(dialogRef, data, remoteUploadService) {
1460
+ this.dialogRef = dialogRef;
1461
+ this.data = data;
1462
+ this.remoteUploadService = remoteUploadService;
1463
+ }
1464
+ ngOnInit() {
1465
+ // Get desktop device ID
1466
+ this.desktopDeviceId = this.remoteUploadService.getDesktopDeviceId();
1467
+ // Subscribe to events first
1468
+ this.subscribeToEvents();
1469
+ // Check if already connected to mobile
1470
+ this.checkExistingConnection();
1471
+ }
1472
+ ngOnDestroy() {
1473
+ this.destroy$.next();
1474
+ this.destroy$.complete();
1475
+ if (this.countdownSubscription) {
1476
+ this.countdownSubscription.unsubscribe();
1477
+ }
1478
+ }
1479
+ /**
1480
+ * Check if there's an existing mobile connection
1481
+ */
1482
+ checkExistingConnection() {
1483
+ this.mobileDeviceId = this.remoteUploadService.getMobileDeviceId();
1484
+ if (this.remoteUploadService.isConnectedToMobile()) {
1485
+ this.isConnected = true;
1486
+ this.connectionStatus = 'connected';
1487
+ this.isLoading = false;
1488
+ }
1489
+ else {
1490
+ // No existing connection, generate QR code
1491
+ this.generateQrCode();
1492
+ }
1493
+ }
1494
+ async generateQrCode() {
1495
+ this.isLoading = true;
1496
+ this.errorMessage = null;
1497
+ this.isExpired = false;
1498
+ try {
1499
+ const result = await this.remoteUploadService.generateQrCode();
1500
+ this.qrData = result.qrData;
1501
+ this.expiresAt = result.expiresAt;
1502
+ this.isLoading = false;
1503
+ this.startCountdown();
1504
+ }
1505
+ catch (error) {
1506
+ this.isLoading = false;
1507
+ this.errorMessage = error.message || 'Failed to generate QR code';
1508
+ }
1509
+ }
1510
+ subscribeToEvents() {
1511
+ // Connection status
1512
+ this.remoteUploadService.getConnectionStatus()
1513
+ .pipe(takeUntil(this.destroy$))
1514
+ .subscribe(status => {
1515
+ this.connectionStatus = status;
1516
+ this.isConnected = status === 'connected';
1517
+ });
1518
+ // Mobile connection changes
1519
+ this.remoteUploadService.getMobileConnection()
1520
+ .pipe(takeUntil(this.destroy$))
1521
+ .subscribe(connection => {
1522
+ this.mobileDeviceId = connection?.mobileDeviceId || null;
1523
+ });
1524
+ // Device online status
1525
+ this.remoteUploadService.getDevicesStatus()
1526
+ .pipe(takeUntil(this.destroy$))
1527
+ .subscribe(status => {
1528
+ this.devicesStatus = status;
1529
+ });
1530
+ // Checking status (for blinking indicator)
1531
+ this.remoteUploadService.getIsCheckingStatus()
1532
+ .pipe(takeUntil(this.destroy$))
1533
+ .subscribe(checking => {
1534
+ this.isCheckingStatus = checking;
1535
+ });
1536
+ // Errors
1537
+ this.remoteUploadService.getErrors()
1538
+ .pipe(takeUntil(this.destroy$))
1539
+ .subscribe(error => {
1540
+ console.error('[AngularQrCodeDialog] Error:', error);
1541
+ });
1542
+ }
1543
+ startCountdown() {
1544
+ if (this.countdownSubscription) {
1545
+ this.countdownSubscription.unsubscribe();
1546
+ }
1547
+ this.countdownSubscription = interval(1000)
1548
+ .pipe(takeUntil(this.destroy$))
1549
+ .subscribe(() => {
1550
+ const now = Date.now();
1551
+ const remaining = Math.max(0, this.expiresAt - now);
1552
+ if (remaining <= 0) {
1553
+ this.isExpired = true;
1554
+ this.remainingTime = 'Expired';
1555
+ this.countdownSubscription?.unsubscribe();
1556
+ }
1557
+ else {
1558
+ const minutes = Math.floor(remaining / 60000);
1559
+ const seconds = Math.floor((remaining % 60000) / 1000);
1560
+ this.remainingTime = `${minutes}:${seconds.toString().padStart(2, '0')}`;
1561
+ }
1562
+ });
1563
+ }
1564
+ // ---------------------------------------------------------------------------
1565
+ // Public Methods
1566
+ // ---------------------------------------------------------------------------
1567
+ /**
1568
+ * Format device ID for display
1569
+ */
1570
+ formatDeviceId(id) {
1571
+ if (!id)
1572
+ return '---';
1573
+ if (id.length > 16) {
1574
+ return `${id.slice(0, 8)}...${id.slice(-4)}`;
1575
+ }
1576
+ return id;
1577
+ }
1578
+ /**
1579
+ * Refresh QR code
1580
+ */
1581
+ refreshQrCode() {
1582
+ this.isExpired = false;
1583
+ this.generateQrCode();
1584
+ }
1585
+ /**
1586
+ * Connect to mobile (show QR if disconnected)
1587
+ */
1588
+ connectToMobile() {
1589
+ this.generateQrCode();
1590
+ }
1591
+ /**
1592
+ * Disconnect from mobile
1593
+ */
1594
+ disconnect() {
1595
+ this.remoteUploadService.disconnect();
1596
+ this.mobileDeviceId = null;
1597
+ this.isConnected = false;
1598
+ this.connectionStatus = 'disconnected';
1599
+ this.devicesStatus = null;
1600
+ }
1601
+ /**
1602
+ * Refresh device status
1603
+ */
1604
+ refreshStatus() {
1605
+ this.remoteUploadService.refreshDevicesStatus();
1606
+ }
1607
+ /**
1608
+ * Get desktop online status indicator class
1609
+ * Returns: 'online' (green), 'offline' (red), 'checking' (blinking yellow)
1610
+ */
1611
+ getDesktopStatusClass() {
1612
+ if (this.isCheckingStatus && !this.devicesStatus) {
1613
+ return 'checking';
1614
+ }
1615
+ return this.devicesStatus?.desktop?.isOnline ? 'online' : 'offline';
1616
+ }
1617
+ /**
1618
+ * Get mobile online status indicator class
1619
+ * Returns: 'online' (green), 'offline' (red), 'checking' (blinking yellow)
1620
+ */
1621
+ getMobileStatusClass() {
1622
+ if (this.isCheckingStatus && !this.devicesStatus) {
1623
+ return 'checking';
1624
+ }
1625
+ return this.devicesStatus?.mobile?.isOnline ? 'online' : 'offline';
1626
+ }
1627
+ /**
1628
+ * Check if ready for transfer (both devices online)
1629
+ */
1630
+ isReadyForTransfer() {
1631
+ return this.devicesStatus?.isReadyForTransfer ?? false;
1632
+ }
1633
+ /**
1634
+ * Close dialog
1635
+ */
1636
+ close() {
1637
+ this.dialogRef.close();
1638
+ }
1639
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularQrCodeDialogComponent, deps: [{ token: i1$2.MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: AngularRemoteUploadService }], target: i0.ɵɵFactoryTarget.Component });
1640
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: AngularQrCodeDialogComponent, isStandalone: false, selector: "angular-qr-code-dialog", ngImport: i0, template: "<div class=\"qr-dialog-container\">\n <!-- Header -->\n <div class=\"qr-dialog-header\">\n <h2 class=\"qr-dialog-title\">{{ data.title || 'Upload from Mobile' }}</h2>\n <p class=\"qr-dialog-subtitle\">{{ data.subtitle || 'Scan this QR code with your mobile device to upload images' }}</p>\n <button mat-icon-button class=\"close-btn\" (click)=\"close()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"qr-dialog-content\">\n <!-- Loading State -->\n <div *ngIf=\"isLoading\" class=\"loading-container\">\n <mat-spinner diameter=\"48\"></mat-spinner>\n <p>Generating QR Code...</p>\n </div>\n\n <!-- Error State -->\n <div *ngIf=\"errorMessage && !isLoading\" class=\"error-container\">\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button mat-raised-button color=\"primary\" (click)=\"refreshQrCode()\">\n Try Again\n </button>\n </div>\n\n <!-- Main Content -->\n <div *ngIf=\"!isLoading && !errorMessage\" class=\"qr-content\">\n \n <!-- Device Info Card -->\n <div class=\"device-info-card\">\n <div class=\"device-row\">\n <div class=\"device-status-dot\" [ngClass]=\"getDesktopStatusClass()\"></div>\n <mat-icon class=\"device-icon desktop\">computer</mat-icon>\n <div class=\"device-details\">\n <span class=\"device-label\">THIS DEVICE</span>\n <span class=\"device-id\" [title]=\"desktopDeviceId\">{{ formatDeviceId(desktopDeviceId) }}</span>\n </div>\n </div>\n \n <div class=\"connection-line\" *ngIf=\"mobileDeviceId\">\n <mat-icon>sync_alt</mat-icon>\n </div>\n \n <div class=\"device-row\" *ngIf=\"mobileDeviceId\">\n <div class=\"device-status-dot\" [ngClass]=\"getMobileStatusClass()\"></div>\n <mat-icon class=\"device-icon mobile\">smartphone</mat-icon>\n <div class=\"device-details\">\n <span class=\"device-label\">CONNECTED MOBILE</span>\n <span class=\"device-id\" [title]=\"mobileDeviceId\">{{ formatDeviceId(mobileDeviceId) }}</span>\n </div>\n </div>\n </div>\n\n <!-- Transfer Status -->\n <div *ngIf=\"isConnected && devicesStatus\" class=\"transfer-status\" [ngClass]=\"{'ready': isReadyForTransfer(), 'not-ready': !isReadyForTransfer()}\">\n <mat-icon>{{ isReadyForTransfer() ? 'check_circle' : 'warning' }}</mat-icon>\n <span *ngIf=\"isReadyForTransfer()\">Ready for transfer</span>\n <span *ngIf=\"!isReadyForTransfer()\">\n {{ !devicesStatus.mobile?.isOnline ? 'Mobile device is offline' : 'Desktop device is offline' }}\n </span>\n <button mat-icon-button class=\"refresh-status-btn\" (click)=\"refreshStatus()\" [disabled]=\"isCheckingStatus\" title=\"Refresh status\">\n <mat-icon [class.spinning]=\"isCheckingStatus\">refresh</mat-icon>\n </button>\n </div>\n\n <!-- Connected State -->\n <div *ngIf=\"isConnected\" class=\"connected-state\">\n <div class=\"connected-indicator\">\n <mat-icon class=\"success-icon\">check_circle</mat-icon>\n <span>Mobile Connected!</span>\n </div>\n <p class=\"connected-message\">\n Your mobile device is now paired with this desktop. Click <strong>\"Done\"</strong> below to close this dialog, \n then click <strong>\"Upload from Mobile\"</strong> whenever you want to upload files.\n </p>\n </div>\n\n <!-- QR Code (only show when not connected) -->\n <div *ngIf=\"!isConnected\" class=\"qr-section\">\n <!-- Connection Status -->\n <div class=\"connection-status\" [ngClass]=\"connectionStatus\">\n <div class=\"status-indicator\"></div>\n <span *ngIf=\"connectionStatus === 'disconnected'\">Waiting for mobile...</span>\n <span *ngIf=\"connectionStatus === 'pending'\">Waiting for connection...</span>\n </div>\n\n <!-- QR Code -->\n <div class=\"qr-code-wrapper\" [class.expired]=\"isExpired\">\n <qrcode \n *ngIf=\"qrData\"\n [qrdata]=\"qrData\" \n [width]=\"data.qrSize || 200\" \n [errorCorrectionLevel]=\"'M'\"\n [elementType]=\"'canvas'\"\n [cssClass]=\"'qr-canvas'\"\n ></qrcode>\n \n <!-- Expired Overlay -->\n <div *ngIf=\"isExpired\" class=\"expired-overlay\">\n <mat-icon>refresh</mat-icon>\n <p>QR Code Expired</p>\n <button mat-raised-button color=\"primary\" (click)=\"refreshQrCode()\">\n Generate New Code\n </button>\n </div>\n </div>\n\n <!-- Countdown Timer -->\n <div class=\"countdown\" *ngIf=\"data.showCountdown !== false && !isExpired\">\n <mat-icon>timer</mat-icon>\n <span>Expires in {{ remainingTime }}</span>\n </div>\n </div>\n\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"qr-dialog-footer\" *ngIf=\"!isLoading\">\n <button mat-button style=\"width: 100%;\" *ngIf=\"!isConnected\" (click)=\"connectToMobile()\">\n <mat-icon>qr_code</mat-icon>\n Regenerate QR Code\n </button>\n <button mat-raised-button style=\"width: 100%;\" *ngIf=\"isConnected\" color=\"primary\" (click)=\"close()\">\n Done\n </button>\n </div>\n</div>\n", styles: [".qr-dialog-container{display:flex;flex-direction:column;width:100%;background:#fff;border-radius:16px;overflow:hidden}.qr-dialog-header{position:relative;padding:24px 24px 16px;text-align:center;border-bottom:1px solid #f0f0f0}.qr-dialog-title{margin:0 0 8px;font-size:20px;font-weight:600;color:#1a1a1a}.qr-dialog-subtitle{margin:0;font-size:14px;color:#666;line-height:1.4}.close-btn{position:absolute;top:12px;right:12px;color:#999}.qr-dialog-content{padding:24px;display:flex;flex-direction:column;align-items:center;min-height:300px}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;height:250px;color:#666}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;height:250px;text-align:center}.error-icon{font-size:48px;width:48px;height:48px;color:#f44336}.error-message{color:#666;margin:0}.qr-content{display:flex;flex-direction:column;align-items:center;gap:10px;width:100%}.qr-section{gap:10px;display:flex;flex-direction:column}.device-info-section{width:100%;display:flex;align-items:center;justify-content:center;gap:12px;padding:16px;background:linear-gradient(135deg,#f8f9fa,#e9ecef);border-radius:12px;margin-bottom:8px}.device-card{display:flex;flex-direction:column;align-items:center;gap:6px;padding:12px 16px;background:#fff;border-radius:10px;min-width:120px;box-shadow:0 2px 8px #0000000f;transition:all .3s ease}.device-card.this-device{border:2px solid #2196f3}.device-card.connected-device{border:2px dashed #9e9e9e}.device-card.connected-device.active{border:2px solid #4caf50}.device-label{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.device-card.this-device .device-label{color:#2196f3}.device-card.connected-device .device-label{color:#9e9e9e}.device-card.connected-device.active .device-label{color:#4caf50}.device-label mat-icon{font-size:14px;width:14px;height:14px}.device-id{font-family:SF Mono,Monaco,Courier New,monospace;font-size:13px;font-weight:600;color:#333;letter-spacing:.5px}.device-id.waiting{color:#9e9e9e;font-style:italic;font-weight:400}.connection-line{display:flex;flex-direction:column;align-items:center;gap:4px;padding:0 8px}.connection-line .line{width:40px;height:2px;background:#e0e0e0;border-radius:1px;position:relative;overflow:hidden}.connection-line.waiting .line:after{content:\"\";position:absolute;top:0;left:-100%;width:50%;height:100%;background:linear-gradient(90deg,transparent,#2196f3,transparent);animation:shimmer 1.5s infinite}.connection-line.connected .line{background:#4caf50}.connection-line mat-icon{font-size:16px;width:16px;height:16px;color:#9e9e9e}.connection-line.waiting mat-icon{color:#ff9800;animation:blink 1s infinite}.connection-line.connected mat-icon{color:#4caf50}@keyframes shimmer{0%{left:-100%}to{left:200%}}.connection-status{display:flex;align-items:center;gap:8px;padding:8px 16px;border-radius:20px;font-size:13px;font-weight:500}.connection-status .status-indicator{width:8px;height:8px;border-radius:50%;animation:pulse 2s infinite}.connection-status.disconnected{background:#f5f5f5;color:#666}.connection-status.disconnected .status-indicator{background:#999}.connection-status.pending{background:#fff3e0;color:#e65100}.connection-status.pending .status-indicator{background:#ff9800}.connection-status.connected{background:#e8f5e9;color:#2e7d32}.connection-status.connected .status-indicator{background:#4caf50;animation:none}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.2)}}.qr-code-wrapper{position:relative;padding:16px;background:#f8f9fa;border-radius:12px;border:2px solid #e0e0e0;transition:all .3s ease}.qr-code-wrapper.expired{opacity:.5}.qr-code-wrapper.connected{border-color:#4caf50;background:#f1f8f4}.qr-canvas{display:block;width:200px;height:200px}.expired-overlay,.connected-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;background:#fffffff2;border-radius:10px}.expired-overlay mat-icon{font-size:40px;width:40px;height:40px;color:#999}.expired-overlay p{margin:0;color:#666;font-weight:500}.connected-overlay .success-icon{font-size:56px;width:56px;height:56px;color:#4caf50}.connected-overlay p{margin:0;color:#2e7d32;font-size:16px;font-weight:600}.pairing-code-section{text-align:center}.pairing-label{margin:0 0 8px;font-size:13px;color:#666}.pairing-code{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;background:#f5f5f5;border-radius:8px;cursor:pointer;transition:background .2s ease}.pairing-code:hover{background:#eee}.pairing-code .code{font-family:SF Mono,Monaco,Courier New,monospace;font-size:18px;font-weight:600;letter-spacing:2px;color:#1a1a1a}.pairing-code .copy-icon{font-size:18px;width:18px;height:18px;color:#666}.countdown{display:flex;align-items:center;gap:6px;font-size:13px;color:#666}.countdown mat-icon{font-size:18px;width:18px;height:18px}.qr-dialog-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #f0f0f0;background:#fafafa}.device-info-card{width:100%;display:flex;flex-direction:row;align-items:center;justify-content:center;gap:12px;padding:12px 16px;background:linear-gradient(135deg,#f8f9fa,#e9ecef);border-radius:12px;margin-bottom:8px}.device-info-card .device-row{display:flex;align-items:center;gap:10px;flex:0 0 auto}.device-info-card .device-icon{font-size:24px;width:24px;height:24px}.device-info-card .device-icon.desktop{color:#1976d2}.device-info-card .device-icon.mobile{color:#7b1fa2}.device-info-card .device-details{display:flex;flex-direction:column;gap:2px}.device-info-card .device-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#666}.device-info-card .device-id{font-family:SF Mono,Monaco,Courier New,monospace;font-size:12px;font-weight:500;color:#333;cursor:default}.device-info-card .connection-line{display:flex;align-items:center;padding:0 4px}.device-info-card .connection-line mat-icon{font-size:18px;width:18px;height:18px;color:#4caf50}@media (max-width: 400px){.qr-dialog-container{min-width:100%;border-radius:0}.qr-canvas{width:180px;height:180px}.device-info-section,.device-info-card{flex-direction:column;gap:8px}.device-info-card .connection-line,.connection-line{transform:rotate(90deg)}.device-card{min-width:100%}}.device-status-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}.device-status-dot.online{background-color:#4caf50;box-shadow:0 0 6px #4caf5080}.device-status-dot.offline{background-color:#f44336;box-shadow:0 0 6px #f4433680}.device-status-dot.checking{background-color:#ff9800;animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.transfer-status{width:100%;display:flex;align-items:center;justify-content:center;gap:8px;padding:10px 16px;border-radius:8px;font-size:13px;font-weight:500}.transfer-status.ready{background-color:#e8f5e9;color:#2e7d32}.transfer-status.ready mat-icon{color:#4caf50}.transfer-status.not-ready{background-color:#fff3e0;color:#e65100}.transfer-status.not-ready mat-icon{color:#ff9800}.refresh-status-btn{margin-left:auto;width:28px;height:28px;line-height:28px;padding:2px}.refresh-status-btn mat-icon{font-size:18px;width:18px;height:18px}.refresh-status-btn mat-icon.spinning{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.connected-indicator{display:flex;justify-content:start;align-items:center;gap:5px;margin-bottom:10px;margin-left:-2px}\n"], dependencies: [{ kind: "directive", type: i3$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i4$2.QRCodeComponent, selector: "qrcode", inputs: ["allowEmptyString", "colorDark", "colorLight", "cssClass", "elementType", "errorCorrectionLevel", "imageSrc", "imageHeight", "imageWidth", "margin", "qrdata", "scale", "version", "width", "alt", "ariaLabel", "title"], outputs: ["qrCodeURL"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i4.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i4$1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }] });
1641
+ }
1642
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularQrCodeDialogComponent, decorators: [{
1643
+ type: Component,
1644
+ args: [{ selector: 'angular-qr-code-dialog', standalone: false, template: "<div class=\"qr-dialog-container\">\n <!-- Header -->\n <div class=\"qr-dialog-header\">\n <h2 class=\"qr-dialog-title\">{{ data.title || 'Upload from Mobile' }}</h2>\n <p class=\"qr-dialog-subtitle\">{{ data.subtitle || 'Scan this QR code with your mobile device to upload images' }}</p>\n <button mat-icon-button class=\"close-btn\" (click)=\"close()\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <!-- Content -->\n <div class=\"qr-dialog-content\">\n <!-- Loading State -->\n <div *ngIf=\"isLoading\" class=\"loading-container\">\n <mat-spinner diameter=\"48\"></mat-spinner>\n <p>Generating QR Code...</p>\n </div>\n\n <!-- Error State -->\n <div *ngIf=\"errorMessage && !isLoading\" class=\"error-container\">\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <p class=\"error-message\">{{ errorMessage }}</p>\n <button mat-raised-button color=\"primary\" (click)=\"refreshQrCode()\">\n Try Again\n </button>\n </div>\n\n <!-- Main Content -->\n <div *ngIf=\"!isLoading && !errorMessage\" class=\"qr-content\">\n \n <!-- Device Info Card -->\n <div class=\"device-info-card\">\n <div class=\"device-row\">\n <div class=\"device-status-dot\" [ngClass]=\"getDesktopStatusClass()\"></div>\n <mat-icon class=\"device-icon desktop\">computer</mat-icon>\n <div class=\"device-details\">\n <span class=\"device-label\">THIS DEVICE</span>\n <span class=\"device-id\" [title]=\"desktopDeviceId\">{{ formatDeviceId(desktopDeviceId) }}</span>\n </div>\n </div>\n \n <div class=\"connection-line\" *ngIf=\"mobileDeviceId\">\n <mat-icon>sync_alt</mat-icon>\n </div>\n \n <div class=\"device-row\" *ngIf=\"mobileDeviceId\">\n <div class=\"device-status-dot\" [ngClass]=\"getMobileStatusClass()\"></div>\n <mat-icon class=\"device-icon mobile\">smartphone</mat-icon>\n <div class=\"device-details\">\n <span class=\"device-label\">CONNECTED MOBILE</span>\n <span class=\"device-id\" [title]=\"mobileDeviceId\">{{ formatDeviceId(mobileDeviceId) }}</span>\n </div>\n </div>\n </div>\n\n <!-- Transfer Status -->\n <div *ngIf=\"isConnected && devicesStatus\" class=\"transfer-status\" [ngClass]=\"{'ready': isReadyForTransfer(), 'not-ready': !isReadyForTransfer()}\">\n <mat-icon>{{ isReadyForTransfer() ? 'check_circle' : 'warning' }}</mat-icon>\n <span *ngIf=\"isReadyForTransfer()\">Ready for transfer</span>\n <span *ngIf=\"!isReadyForTransfer()\">\n {{ !devicesStatus.mobile?.isOnline ? 'Mobile device is offline' : 'Desktop device is offline' }}\n </span>\n <button mat-icon-button class=\"refresh-status-btn\" (click)=\"refreshStatus()\" [disabled]=\"isCheckingStatus\" title=\"Refresh status\">\n <mat-icon [class.spinning]=\"isCheckingStatus\">refresh</mat-icon>\n </button>\n </div>\n\n <!-- Connected State -->\n <div *ngIf=\"isConnected\" class=\"connected-state\">\n <div class=\"connected-indicator\">\n <mat-icon class=\"success-icon\">check_circle</mat-icon>\n <span>Mobile Connected!</span>\n </div>\n <p class=\"connected-message\">\n Your mobile device is now paired with this desktop. Click <strong>\"Done\"</strong> below to close this dialog, \n then click <strong>\"Upload from Mobile\"</strong> whenever you want to upload files.\n </p>\n </div>\n\n <!-- QR Code (only show when not connected) -->\n <div *ngIf=\"!isConnected\" class=\"qr-section\">\n <!-- Connection Status -->\n <div class=\"connection-status\" [ngClass]=\"connectionStatus\">\n <div class=\"status-indicator\"></div>\n <span *ngIf=\"connectionStatus === 'disconnected'\">Waiting for mobile...</span>\n <span *ngIf=\"connectionStatus === 'pending'\">Waiting for connection...</span>\n </div>\n\n <!-- QR Code -->\n <div class=\"qr-code-wrapper\" [class.expired]=\"isExpired\">\n <qrcode \n *ngIf=\"qrData\"\n [qrdata]=\"qrData\" \n [width]=\"data.qrSize || 200\" \n [errorCorrectionLevel]=\"'M'\"\n [elementType]=\"'canvas'\"\n [cssClass]=\"'qr-canvas'\"\n ></qrcode>\n \n <!-- Expired Overlay -->\n <div *ngIf=\"isExpired\" class=\"expired-overlay\">\n <mat-icon>refresh</mat-icon>\n <p>QR Code Expired</p>\n <button mat-raised-button color=\"primary\" (click)=\"refreshQrCode()\">\n Generate New Code\n </button>\n </div>\n </div>\n\n <!-- Countdown Timer -->\n <div class=\"countdown\" *ngIf=\"data.showCountdown !== false && !isExpired\">\n <mat-icon>timer</mat-icon>\n <span>Expires in {{ remainingTime }}</span>\n </div>\n </div>\n\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"qr-dialog-footer\" *ngIf=\"!isLoading\">\n <button mat-button style=\"width: 100%;\" *ngIf=\"!isConnected\" (click)=\"connectToMobile()\">\n <mat-icon>qr_code</mat-icon>\n Regenerate QR Code\n </button>\n <button mat-raised-button style=\"width: 100%;\" *ngIf=\"isConnected\" color=\"primary\" (click)=\"close()\">\n Done\n </button>\n </div>\n</div>\n", styles: [".qr-dialog-container{display:flex;flex-direction:column;width:100%;background:#fff;border-radius:16px;overflow:hidden}.qr-dialog-header{position:relative;padding:24px 24px 16px;text-align:center;border-bottom:1px solid #f0f0f0}.qr-dialog-title{margin:0 0 8px;font-size:20px;font-weight:600;color:#1a1a1a}.qr-dialog-subtitle{margin:0;font-size:14px;color:#666;line-height:1.4}.close-btn{position:absolute;top:12px;right:12px;color:#999}.qr-dialog-content{padding:24px;display:flex;flex-direction:column;align-items:center;min-height:300px}.loading-container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;height:250px;color:#666}.error-container{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;height:250px;text-align:center}.error-icon{font-size:48px;width:48px;height:48px;color:#f44336}.error-message{color:#666;margin:0}.qr-content{display:flex;flex-direction:column;align-items:center;gap:10px;width:100%}.qr-section{gap:10px;display:flex;flex-direction:column}.device-info-section{width:100%;display:flex;align-items:center;justify-content:center;gap:12px;padding:16px;background:linear-gradient(135deg,#f8f9fa,#e9ecef);border-radius:12px;margin-bottom:8px}.device-card{display:flex;flex-direction:column;align-items:center;gap:6px;padding:12px 16px;background:#fff;border-radius:10px;min-width:120px;box-shadow:0 2px 8px #0000000f;transition:all .3s ease}.device-card.this-device{border:2px solid #2196f3}.device-card.connected-device{border:2px dashed #9e9e9e}.device-card.connected-device.active{border:2px solid #4caf50}.device-label{display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.device-card.this-device .device-label{color:#2196f3}.device-card.connected-device .device-label{color:#9e9e9e}.device-card.connected-device.active .device-label{color:#4caf50}.device-label mat-icon{font-size:14px;width:14px;height:14px}.device-id{font-family:SF Mono,Monaco,Courier New,monospace;font-size:13px;font-weight:600;color:#333;letter-spacing:.5px}.device-id.waiting{color:#9e9e9e;font-style:italic;font-weight:400}.connection-line{display:flex;flex-direction:column;align-items:center;gap:4px;padding:0 8px}.connection-line .line{width:40px;height:2px;background:#e0e0e0;border-radius:1px;position:relative;overflow:hidden}.connection-line.waiting .line:after{content:\"\";position:absolute;top:0;left:-100%;width:50%;height:100%;background:linear-gradient(90deg,transparent,#2196f3,transparent);animation:shimmer 1.5s infinite}.connection-line.connected .line{background:#4caf50}.connection-line mat-icon{font-size:16px;width:16px;height:16px;color:#9e9e9e}.connection-line.waiting mat-icon{color:#ff9800;animation:blink 1s infinite}.connection-line.connected mat-icon{color:#4caf50}@keyframes shimmer{0%{left:-100%}to{left:200%}}.connection-status{display:flex;align-items:center;gap:8px;padding:8px 16px;border-radius:20px;font-size:13px;font-weight:500}.connection-status .status-indicator{width:8px;height:8px;border-radius:50%;animation:pulse 2s infinite}.connection-status.disconnected{background:#f5f5f5;color:#666}.connection-status.disconnected .status-indicator{background:#999}.connection-status.pending{background:#fff3e0;color:#e65100}.connection-status.pending .status-indicator{background:#ff9800}.connection-status.connected{background:#e8f5e9;color:#2e7d32}.connection-status.connected .status-indicator{background:#4caf50;animation:none}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(1.2)}}.qr-code-wrapper{position:relative;padding:16px;background:#f8f9fa;border-radius:12px;border:2px solid #e0e0e0;transition:all .3s ease}.qr-code-wrapper.expired{opacity:.5}.qr-code-wrapper.connected{border-color:#4caf50;background:#f1f8f4}.qr-canvas{display:block;width:200px;height:200px}.expired-overlay,.connected-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;background:#fffffff2;border-radius:10px}.expired-overlay mat-icon{font-size:40px;width:40px;height:40px;color:#999}.expired-overlay p{margin:0;color:#666;font-weight:500}.connected-overlay .success-icon{font-size:56px;width:56px;height:56px;color:#4caf50}.connected-overlay p{margin:0;color:#2e7d32;font-size:16px;font-weight:600}.pairing-code-section{text-align:center}.pairing-label{margin:0 0 8px;font-size:13px;color:#666}.pairing-code{display:inline-flex;align-items:center;gap:8px;padding:10px 16px;background:#f5f5f5;border-radius:8px;cursor:pointer;transition:background .2s ease}.pairing-code:hover{background:#eee}.pairing-code .code{font-family:SF Mono,Monaco,Courier New,monospace;font-size:18px;font-weight:600;letter-spacing:2px;color:#1a1a1a}.pairing-code .copy-icon{font-size:18px;width:18px;height:18px;color:#666}.countdown{display:flex;align-items:center;gap:6px;font-size:13px;color:#666}.countdown mat-icon{font-size:18px;width:18px;height:18px}.qr-dialog-footer{display:flex;justify-content:flex-end;gap:12px;padding:16px 24px;border-top:1px solid #f0f0f0;background:#fafafa}.device-info-card{width:100%;display:flex;flex-direction:row;align-items:center;justify-content:center;gap:12px;padding:12px 16px;background:linear-gradient(135deg,#f8f9fa,#e9ecef);border-radius:12px;margin-bottom:8px}.device-info-card .device-row{display:flex;align-items:center;gap:10px;flex:0 0 auto}.device-info-card .device-icon{font-size:24px;width:24px;height:24px}.device-info-card .device-icon.desktop{color:#1976d2}.device-info-card .device-icon.mobile{color:#7b1fa2}.device-info-card .device-details{display:flex;flex-direction:column;gap:2px}.device-info-card .device-label{font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:#666}.device-info-card .device-id{font-family:SF Mono,Monaco,Courier New,monospace;font-size:12px;font-weight:500;color:#333;cursor:default}.device-info-card .connection-line{display:flex;align-items:center;padding:0 4px}.device-info-card .connection-line mat-icon{font-size:18px;width:18px;height:18px;color:#4caf50}@media (max-width: 400px){.qr-dialog-container{min-width:100%;border-radius:0}.qr-canvas{width:180px;height:180px}.device-info-section,.device-info-card{flex-direction:column;gap:8px}.device-info-card .connection-line,.connection-line{transform:rotate(90deg)}.device-card{min-width:100%}}.device-status-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}.device-status-dot.online{background-color:#4caf50;box-shadow:0 0 6px #4caf5080}.device-status-dot.offline{background-color:#f44336;box-shadow:0 0 6px #f4433680}.device-status-dot.checking{background-color:#ff9800;animation:blink 1s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.transfer-status{width:100%;display:flex;align-items:center;justify-content:center;gap:8px;padding:10px 16px;border-radius:8px;font-size:13px;font-weight:500}.transfer-status.ready{background-color:#e8f5e9;color:#2e7d32}.transfer-status.ready mat-icon{color:#4caf50}.transfer-status.not-ready{background-color:#fff3e0;color:#e65100}.transfer-status.not-ready mat-icon{color:#ff9800}.refresh-status-btn{margin-left:auto;width:28px;height:28px;line-height:28px;padding:2px}.refresh-status-btn mat-icon{font-size:18px;width:18px;height:18px}.refresh-status-btn mat-icon.spinning{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.connected-indicator{display:flex;justify-content:start;align-items:center;gap:5px;margin-bottom:10px;margin-left:-2px}\n"] }]
1645
+ }], ctorParameters: () => [{ type: i1$2.MatDialogRef }, { type: undefined, decorators: [{
1646
+ type: Inject,
1647
+ args: [MAT_DIALOG_DATA]
1648
+ }] }, { type: AngularRemoteUploadService }] });
1649
+
1650
+ class TimeAgoPipe {
1651
+ transform(timestamp, format = 'short') {
1652
+ if (!timestamp) {
1653
+ return 'Never';
1654
+ }
1655
+ const secondsAgo = Math.floor((Date.now() - timestamp) / 1000);
1656
+ if (format === 'long') {
1657
+ // For connection duration
1658
+ const minutes = Math.floor(secondsAgo / 60);
1659
+ const hours = Math.floor(minutes / 60);
1660
+ if (hours > 0) {
1661
+ const remainingMinutes = minutes % 60;
1662
+ return `${hours}h ${remainingMinutes}m`;
1663
+ }
1664
+ else if (minutes > 0) {
1665
+ return `${minutes}m`;
1666
+ }
1667
+ else {
1668
+ return 'Just now';
1669
+ }
1670
+ }
1671
+ else {
1672
+ // For last ping
1673
+ if (secondsAgo < 10) {
1674
+ return 'Just now';
1675
+ }
1676
+ else if (secondsAgo < 60) {
1677
+ return `${secondsAgo}s ago`;
1678
+ }
1679
+ else {
1680
+ const minutesAgo = Math.floor(secondsAgo / 60);
1681
+ return `${minutesAgo}m ago`;
1682
+ }
1683
+ }
1684
+ }
1685
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: TimeAgoPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
1686
+ static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.2.6", ngImport: i0, type: TimeAgoPipe, isStandalone: false, name: "timeAgo" });
1687
+ }
1688
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: TimeAgoPipe, decorators: [{
1689
+ type: Pipe,
1690
+ args: [{
1691
+ name: 'timeAgo',
1692
+ standalone: false,
1693
+ pure: true
1694
+ }]
1695
+ }] });
1696
+
1697
+ class AngularViewConnectionDialogComponent {
1698
+ dialogRef;
1699
+ data;
1700
+ remoteUploadService;
1701
+ destroy$ = new Subject();
1702
+ mobileConnection = null;
1703
+ devicesStatus = null;
1704
+ isCheckingStatus = false;
1705
+ disconnecting = false;
1706
+ constructor(dialogRef, data, remoteUploadService) {
1707
+ this.dialogRef = dialogRef;
1708
+ this.data = data;
1709
+ this.remoteUploadService = remoteUploadService;
1710
+ }
1711
+ ngOnInit() {
1712
+ // Subscribe to mobile connection info
1713
+ this.remoteUploadService.getMobileConnection()
1714
+ .pipe(takeUntil$1(this.destroy$))
1715
+ .subscribe(connection => {
1716
+ this.mobileConnection = connection;
1717
+ // If connection is lost, close dialog
1718
+ if (!connection) {
1719
+ this.dialogRef.close('disconnected');
1720
+ }
1721
+ });
1722
+ // Subscribe to devices status
1723
+ this.remoteUploadService.getDevicesStatus()
1724
+ .pipe(takeUntil$1(this.destroy$))
1725
+ .subscribe(status => {
1726
+ this.devicesStatus = status;
1727
+ });
1728
+ // Subscribe to checking status
1729
+ this.remoteUploadService.getIsCheckingStatus()
1730
+ .pipe(takeUntil$1(this.destroy$))
1731
+ .subscribe(checking => {
1732
+ this.isCheckingStatus = checking;
1733
+ });
1734
+ // Refresh status on open
1735
+ this.refreshStatus();
1736
+ }
1737
+ ngOnDestroy() {
1738
+ this.destroy$.next();
1739
+ this.destroy$.complete();
1740
+ }
1741
+ /**
1742
+ * Refresh device online status
1743
+ */
1744
+ async refreshStatus() {
1745
+ await this.remoteUploadService.refreshDevicesStatus();
1746
+ }
1747
+ /**
1748
+ * Disconnect from mobile device
1749
+ */
1750
+ async disconnect() {
1751
+ this.disconnecting = true;
1752
+ try {
1753
+ await this.remoteUploadService.disconnect();
1754
+ this.dialogRef.close('disconnected');
1755
+ }
1756
+ catch (error) {
1757
+ console.error('Disconnect failed:', error);
1758
+ this.disconnecting = false;
1759
+ }
1760
+ }
1761
+ /**
1762
+ * Close dialog without disconnecting
1763
+ */
1764
+ close() {
1765
+ this.dialogRef.close();
1766
+ }
1767
+ /**
1768
+ * Truncate device ID to show first 8 and last 8 characters
1769
+ */
1770
+ truncateDeviceId(deviceId) {
1771
+ if (!deviceId || deviceId.length <= 20) {
1772
+ return deviceId;
1773
+ }
1774
+ return `${deviceId.substring(0, 8)}...${deviceId.substring(deviceId.length - 8)}`;
1775
+ }
1776
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularViewConnectionDialogComponent, deps: [{ token: i1$2.MatDialogRef }, { token: MAT_DIALOG_DATA }, { token: AngularRemoteUploadService }], target: i0.ɵɵFactoryTarget.Component });
1777
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: AngularViewConnectionDialogComponent, isStandalone: false, selector: "angular-view-connection-dialog", ngImport: i0, template: "<div class=\"dialog-container\">\n <div class=\"dialog-header\">\n <h2 mat-dialog-title>{{ data.title || 'Mobile Connection' }}</h2>\n <button mat-icon-button class=\"close-btn\" (click)=\"close()\" tabindex=\"-1\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <mat-dialog-content class=\"dialog-content\">\n @if (mobileConnection) {\n <!-- Connection Info Card -->\n <div class=\"connection-card elevation-card\">\n <div class=\"connection-header\">\n <div class=\"status-indicator\" [class.online]=\"devicesStatus?.isReadyForTransfer\" [class.blinking]=\"isCheckingStatus\">\n <mat-icon>{{ devicesStatus?.isReadyForTransfer ? 'check_circle' : 'error' }}</mat-icon>\n <span class=\"status-text\">\n {{ devicesStatus?.isReadyForTransfer ? 'Connected' : 'Connection Issue' }}\n </span>\n </div>\n <button \n mat-stroked-button\n color=\"primary\" \n class=\"refresh-btn\"\n (click)=\"refreshStatus()\"\n [disabled]=\"isCheckingStatus\">\n <mat-icon [class.spinning]=\"isCheckingStatus\">refresh</mat-icon>\n <span class=\"refresh-text\">Refresh</span>\n </button>\n </div>\n\n <div class=\"connection-details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Mobile Device ID</span>\n <span class=\"detail-value\">{{ truncateDeviceId(mobileConnection.mobileDeviceId) }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Connected For</span>\n <span class=\"detail-value\">{{ mobileConnection.connectedAt | timeAgo:'long' }}</span>\n </div>\n @if (mobileConnection.connectedAt) {\n <div class=\"detail-row\">\n <span class=\"detail-label\">Connected At</span>\n <span class=\"detail-value\">{{ mobileConnection.connectedAt | date:'short' }}</span>\n </div>\n }\n </div>\n </div>\n\n <!-- Devices Status Card -->\n @if (devicesStatus) {\n <div class=\"devices-status-card elevation-card\">\n <h3 class=\"card-title\">Device Status</h3>\n \n <div class=\"device-status-row\">\n <div class=\"device-info\">\n <mat-icon class=\"device-icon\">computer</mat-icon>\n <div class=\"device-details\">\n <div class=\"device-name\">Desktop</div>\n <div class=\"device-id\">{{ truncateDeviceId(devicesStatus.desktop.deviceId) }}</div>\n </div>\n </div>\n <div class=\"device-status-info\">\n <div class=\"status-badge\" [class.online]=\"devicesStatus.desktop.isOnline\">\n <mat-icon>{{ devicesStatus.desktop.isOnline ? 'circle' : 'cancel' }}</mat-icon>\n <span>{{ devicesStatus.desktop.isOnline ? 'Online' : 'Offline' }}</span>\n </div>\n @if (devicesStatus.desktop.lastPing) {\n <div class=\"last-ping\">{{ devicesStatus.desktop.lastPing | timeAgo }}</div>\n }\n </div>\n </div>\n\n <mat-divider></mat-divider>\n\n <div class=\"device-status-row\">\n <div class=\"device-info\">\n <mat-icon class=\"device-icon\">smartphone</mat-icon>\n <div class=\"device-details\">\n <div class=\"device-name\">Mobile</div>\n <div class=\"device-id\">{{ truncateDeviceId(devicesStatus.mobile.deviceId) }}</div>\n </div>\n </div>\n <div class=\"device-status-info\">\n <div class=\"status-badge\" [class.online]=\"devicesStatus.mobile.isOnline\">\n <mat-icon>{{ devicesStatus.mobile.isOnline ? 'circle' : 'cancel' }}</mat-icon>\n <span>{{ devicesStatus.mobile.isOnline ? 'Online' : 'Offline' }}</span>\n </div>\n @if (devicesStatus.mobile.lastPing) {\n <div class=\"last-ping\">{{ devicesStatus.mobile.lastPing | timeAgo }}</div>\n }\n </div>\n </div>\n </div>\n }\n\n <!-- Warning Message -->\n @if (devicesStatus && !devicesStatus.isReadyForTransfer) {\n <div class=\"warning-message\">\n <mat-icon>warning</mat-icon>\n <span>File uploads may not work properly when devices are offline.</span>\n </div>\n }\n } @else {\n <!-- No Connection -->\n <div class=\"no-connection\">\n <mat-icon>link_off</mat-icon>\n <p>No mobile device connected</p>\n </div>\n }\n </mat-dialog-content>\n\n <mat-dialog-actions align=\"end\" class=\"dialog-actions\">\n <button \n mat-button \n (click)=\"close()\"\n [disabled]=\"disconnecting\">\n Close\n </button>\n @if (mobileConnection) {\n <button \n mat-raised-button \n color=\"warn\" \n (click)=\"disconnect()\"\n [disabled]=\"disconnecting\">\n <mat-icon *ngIf=\"!disconnecting\">link_off</mat-icon>\n <mat-spinner *ngIf=\"disconnecting\" diameter=\"18\"></mat-spinner>\n <span>{{ disconnecting ? 'Disconnecting...' : 'Disconnect' }}</span>\n </button>\n }\n </mat-dialog-actions>\n</div>\n", styles: [".dialog-container{min-width:500px;max-width:600px}@media (max-width: 600px){.dialog-container{min-width:unset;width:100vw;max-width:100vw}}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px 0}@media (max-width: 600px){.dialog-header{padding:12px 16px 0}}.dialog-header h2{margin:0;font-size:20px;font-weight:500;color:#1a1a1a}@media (max-width: 600px){.dialog-header h2{font-size:18px}}.close-btn{margin-right:-12px}.dialog-content{padding:16px 24px!important;display:flex;flex-direction:column;gap:16px}@media (max-width: 600px){.dialog-content{padding:12px 16px!important;gap:12px}}.dialog-actions{padding:8px 16px 16px!important}.elevation-card{box-shadow:0 2px 8px #0000001a;transition:box-shadow .3s ease}.elevation-card:hover{box-shadow:0 4px 12px #00000026}.connection-card{background:linear-gradient(135deg,#f8f9fa,#e9ecef);border-radius:12px;padding:20px;border:1px solid rgba(0,0,0,.05)}@media (max-width: 600px){.connection-card{padding:16px;border-radius:8px}}.connection-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}@media (max-width: 600px){.connection-header{margin-bottom:16px}}.status-indicator{display:flex;align-items:center;gap:10px;font-weight:600;font-size:15px;padding:8px 16px;border-radius:8px;background:#ffffffb3;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media (max-width: 600px){.status-indicator{font-size:14px;padding:6px 12px;gap:8px}}.status-indicator mat-icon{font-size:28px;width:28px;height:28px}@media (max-width: 600px){.status-indicator mat-icon{font-size:24px;width:24px;height:24px}}.status-indicator.online{color:#2e7d32;background:#4caf5026;border:1px solid rgba(76,175,80,.3)}.status-indicator:not(.online){color:#c62828;background:#f4433626;border:1px solid rgba(244,67,54,.3)}.status-indicator.blinking mat-icon{animation:blink 1.5s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.refresh-btn{min-width:auto;font-weight:500}.refresh-btn mat-icon{margin-right:6px}@media (max-width: 600px){.refresh-btn .refresh-text{display:none}.refresh-btn mat-icon{margin-right:0}}.refresh-btn mat-icon.spinning{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.connection-details{display:flex;flex-direction:column;gap:12px}.detail-row{display:flex;justify-content:space-between;font-size:14px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,.06)}.detail-row:last-child{border-bottom:none}@media (max-width: 600px){.detail-row{font-size:13px;flex-direction:column;gap:4px;padding:10px 0}}.detail-label{color:#616161;font-weight:600;font-size:13px;text-transform:uppercase;letter-spacing:.5px}@media (max-width: 600px){.detail-label{font-size:11px}}.detail-value{color:#1a1a1a;font-family:Courier New,monospace;font-weight:500;font-size:14px}@media (max-width: 600px){.detail-value{font-size:13px;word-break:break-all}}.devices-status-card{border:1px solid rgba(0,0,0,.08);border-radius:12px;padding:20px;background:#fff}@media (max-width: 600px){.devices-status-card{padding:16px;border-radius:8px}}.card-title{margin:0 0 20px;font-size:16px;font-weight:600;color:#1a1a1a;text-transform:uppercase;letter-spacing:.5px}@media (max-width: 600px){.card-title{font-size:14px;margin:0 0 16px}}.device-status-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 0}@media (max-width: 600px){.device-status-row{flex-wrap:wrap;gap:12px;padding:12px 0}}.device-info{display:flex;align-items:center;gap:16px;flex:1;min-width:0}@media (max-width: 600px){.device-info{gap:12px;flex:1 1 100%}}.device-icon{font-size:36px;width:36px;height:36px;color:#1976d2;flex-shrink:0}@media (max-width: 600px){.device-icon{font-size:32px;width:32px;height:32px}}.device-details{flex:1;min-width:0}.device-name{font-weight:600;font-size:15px;color:#1a1a1a;margin-bottom:4px}@media (max-width: 600px){.device-name{font-size:14px}}.device-id{font-size:12px;color:#757575;font-family:Courier New,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (max-width: 600px){.device-id{font-size:11px}}.device-status-info{display:flex;flex-direction:column;align-items:flex-end;gap:6px}@media (max-width: 600px){.device-status-info{flex:1 1 100%;flex-direction:row;justify-content:space-between;align-items:center}}.status-badge{display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:20px;font-size:13px;font-weight:600;background:#ffebee;color:#c62828;border:1px solid rgba(198,40,40,.2);box-shadow:0 2px 4px #0000001a}@media (max-width: 600px){.status-badge{font-size:12px;padding:5px 12px}}.status-badge.online{background:#e8f5e9;color:#2e7d32;border:1px solid rgba(46,125,50,.2)}.status-badge mat-icon{font-size:16px;width:16px;height:16px}.last-ping{font-size:12px;color:#9e9e9e;font-weight:500;white-space:nowrap}@media (max-width: 600px){.last-ping{font-size:11px}}mat-divider{margin:12px 0}@media (max-width: 600px){mat-divider{margin:8px 0}}.warning-message{display:flex;align-items:center;gap:12px;padding:14px 16px;background:linear-gradient(135deg,#fff3e0,#ffe0b2);border-left:4px solid #ff9800;border-radius:8px;color:#e65100;font-size:14px;font-weight:500;box-shadow:0 2px 6px #ff980026}@media (max-width: 600px){.warning-message{font-size:13px;padding:12px 14px;gap:10px}}.warning-message mat-icon{font-size:24px;width:24px;height:24px;flex-shrink:0}@media (max-width: 600px){.warning-message mat-icon{font-size:20px;width:20px;height:20px}}.no-connection{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 40px;text-align:center;color:#bdbdbd;background:#fafafa;border-radius:12px;border:2px dashed #e0e0e0}@media (max-width: 600px){.no-connection{padding:40px 20px}}.no-connection mat-icon{font-size:72px;width:72px;height:72px;margin-bottom:20px;opacity:.4}@media (max-width: 600px){.no-connection mat-icon{font-size:56px;width:56px;height:56px;margin-bottom:16px}}.no-connection p{margin:0;font-size:16px;font-weight:500;color:#757575}@media (max-width: 600px){.no-connection p{font-size:14px}}.dialog-actions button{margin-left:8px}.dialog-actions button mat-spinner{display:inline-block;margin-right:8px}\n"], dependencies: [{ kind: "directive", type: i3$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i4.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i4$1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "directive", type: i1$2.MatDialogTitle, selector: "[mat-dialog-title], [matDialogTitle]", inputs: ["id"], exportAs: ["matDialogTitle"] }, { kind: "directive", type: i1$2.MatDialogActions, selector: "[mat-dialog-actions], mat-dialog-actions, [matDialogActions]", inputs: ["align"] }, { kind: "directive", type: i1$2.MatDialogContent, selector: "[mat-dialog-content], mat-dialog-content, [matDialogContent]" }, { kind: "component", type: i7.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "pipe", type: i3$2.DatePipe, name: "date" }, { kind: "pipe", type: TimeAgoPipe, name: "timeAgo" }] });
1778
+ }
1779
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularViewConnectionDialogComponent, decorators: [{
1780
+ type: Component,
1781
+ args: [{ selector: 'angular-view-connection-dialog', standalone: false, template: "<div class=\"dialog-container\">\n <div class=\"dialog-header\">\n <h2 mat-dialog-title>{{ data.title || 'Mobile Connection' }}</h2>\n <button mat-icon-button class=\"close-btn\" (click)=\"close()\" tabindex=\"-1\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n\n <mat-dialog-content class=\"dialog-content\">\n @if (mobileConnection) {\n <!-- Connection Info Card -->\n <div class=\"connection-card elevation-card\">\n <div class=\"connection-header\">\n <div class=\"status-indicator\" [class.online]=\"devicesStatus?.isReadyForTransfer\" [class.blinking]=\"isCheckingStatus\">\n <mat-icon>{{ devicesStatus?.isReadyForTransfer ? 'check_circle' : 'error' }}</mat-icon>\n <span class=\"status-text\">\n {{ devicesStatus?.isReadyForTransfer ? 'Connected' : 'Connection Issue' }}\n </span>\n </div>\n <button \n mat-stroked-button\n color=\"primary\" \n class=\"refresh-btn\"\n (click)=\"refreshStatus()\"\n [disabled]=\"isCheckingStatus\">\n <mat-icon [class.spinning]=\"isCheckingStatus\">refresh</mat-icon>\n <span class=\"refresh-text\">Refresh</span>\n </button>\n </div>\n\n <div class=\"connection-details\">\n <div class=\"detail-row\">\n <span class=\"detail-label\">Mobile Device ID</span>\n <span class=\"detail-value\">{{ truncateDeviceId(mobileConnection.mobileDeviceId) }}</span>\n </div>\n <div class=\"detail-row\">\n <span class=\"detail-label\">Connected For</span>\n <span class=\"detail-value\">{{ mobileConnection.connectedAt | timeAgo:'long' }}</span>\n </div>\n @if (mobileConnection.connectedAt) {\n <div class=\"detail-row\">\n <span class=\"detail-label\">Connected At</span>\n <span class=\"detail-value\">{{ mobileConnection.connectedAt | date:'short' }}</span>\n </div>\n }\n </div>\n </div>\n\n <!-- Devices Status Card -->\n @if (devicesStatus) {\n <div class=\"devices-status-card elevation-card\">\n <h3 class=\"card-title\">Device Status</h3>\n \n <div class=\"device-status-row\">\n <div class=\"device-info\">\n <mat-icon class=\"device-icon\">computer</mat-icon>\n <div class=\"device-details\">\n <div class=\"device-name\">Desktop</div>\n <div class=\"device-id\">{{ truncateDeviceId(devicesStatus.desktop.deviceId) }}</div>\n </div>\n </div>\n <div class=\"device-status-info\">\n <div class=\"status-badge\" [class.online]=\"devicesStatus.desktop.isOnline\">\n <mat-icon>{{ devicesStatus.desktop.isOnline ? 'circle' : 'cancel' }}</mat-icon>\n <span>{{ devicesStatus.desktop.isOnline ? 'Online' : 'Offline' }}</span>\n </div>\n @if (devicesStatus.desktop.lastPing) {\n <div class=\"last-ping\">{{ devicesStatus.desktop.lastPing | timeAgo }}</div>\n }\n </div>\n </div>\n\n <mat-divider></mat-divider>\n\n <div class=\"device-status-row\">\n <div class=\"device-info\">\n <mat-icon class=\"device-icon\">smartphone</mat-icon>\n <div class=\"device-details\">\n <div class=\"device-name\">Mobile</div>\n <div class=\"device-id\">{{ truncateDeviceId(devicesStatus.mobile.deviceId) }}</div>\n </div>\n </div>\n <div class=\"device-status-info\">\n <div class=\"status-badge\" [class.online]=\"devicesStatus.mobile.isOnline\">\n <mat-icon>{{ devicesStatus.mobile.isOnline ? 'circle' : 'cancel' }}</mat-icon>\n <span>{{ devicesStatus.mobile.isOnline ? 'Online' : 'Offline' }}</span>\n </div>\n @if (devicesStatus.mobile.lastPing) {\n <div class=\"last-ping\">{{ devicesStatus.mobile.lastPing | timeAgo }}</div>\n }\n </div>\n </div>\n </div>\n }\n\n <!-- Warning Message -->\n @if (devicesStatus && !devicesStatus.isReadyForTransfer) {\n <div class=\"warning-message\">\n <mat-icon>warning</mat-icon>\n <span>File uploads may not work properly when devices are offline.</span>\n </div>\n }\n } @else {\n <!-- No Connection -->\n <div class=\"no-connection\">\n <mat-icon>link_off</mat-icon>\n <p>No mobile device connected</p>\n </div>\n }\n </mat-dialog-content>\n\n <mat-dialog-actions align=\"end\" class=\"dialog-actions\">\n <button \n mat-button \n (click)=\"close()\"\n [disabled]=\"disconnecting\">\n Close\n </button>\n @if (mobileConnection) {\n <button \n mat-raised-button \n color=\"warn\" \n (click)=\"disconnect()\"\n [disabled]=\"disconnecting\">\n <mat-icon *ngIf=\"!disconnecting\">link_off</mat-icon>\n <mat-spinner *ngIf=\"disconnecting\" diameter=\"18\"></mat-spinner>\n <span>{{ disconnecting ? 'Disconnecting...' : 'Disconnect' }}</span>\n </button>\n }\n </mat-dialog-actions>\n</div>\n", styles: [".dialog-container{min-width:500px;max-width:600px}@media (max-width: 600px){.dialog-container{min-width:unset;width:100vw;max-width:100vw}}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px 0}@media (max-width: 600px){.dialog-header{padding:12px 16px 0}}.dialog-header h2{margin:0;font-size:20px;font-weight:500;color:#1a1a1a}@media (max-width: 600px){.dialog-header h2{font-size:18px}}.close-btn{margin-right:-12px}.dialog-content{padding:16px 24px!important;display:flex;flex-direction:column;gap:16px}@media (max-width: 600px){.dialog-content{padding:12px 16px!important;gap:12px}}.dialog-actions{padding:8px 16px 16px!important}.elevation-card{box-shadow:0 2px 8px #0000001a;transition:box-shadow .3s ease}.elevation-card:hover{box-shadow:0 4px 12px #00000026}.connection-card{background:linear-gradient(135deg,#f8f9fa,#e9ecef);border-radius:12px;padding:20px;border:1px solid rgba(0,0,0,.05)}@media (max-width: 600px){.connection-card{padding:16px;border-radius:8px}}.connection-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:12px}@media (max-width: 600px){.connection-header{margin-bottom:16px}}.status-indicator{display:flex;align-items:center;gap:10px;font-weight:600;font-size:15px;padding:8px 16px;border-radius:8px;background:#ffffffb3;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}@media (max-width: 600px){.status-indicator{font-size:14px;padding:6px 12px;gap:8px}}.status-indicator mat-icon{font-size:28px;width:28px;height:28px}@media (max-width: 600px){.status-indicator mat-icon{font-size:24px;width:24px;height:24px}}.status-indicator.online{color:#2e7d32;background:#4caf5026;border:1px solid rgba(76,175,80,.3)}.status-indicator:not(.online){color:#c62828;background:#f4433626;border:1px solid rgba(244,67,54,.3)}.status-indicator.blinking mat-icon{animation:blink 1.5s ease-in-out infinite}@keyframes blink{0%,to{opacity:1}50%{opacity:.3}}.refresh-btn{min-width:auto;font-weight:500}.refresh-btn mat-icon{margin-right:6px}@media (max-width: 600px){.refresh-btn .refresh-text{display:none}.refresh-btn mat-icon{margin-right:0}}.refresh-btn mat-icon.spinning{animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.connection-details{display:flex;flex-direction:column;gap:12px}.detail-row{display:flex;justify-content:space-between;font-size:14px;padding:8px 0;border-bottom:1px solid rgba(0,0,0,.06)}.detail-row:last-child{border-bottom:none}@media (max-width: 600px){.detail-row{font-size:13px;flex-direction:column;gap:4px;padding:10px 0}}.detail-label{color:#616161;font-weight:600;font-size:13px;text-transform:uppercase;letter-spacing:.5px}@media (max-width: 600px){.detail-label{font-size:11px}}.detail-value{color:#1a1a1a;font-family:Courier New,monospace;font-weight:500;font-size:14px}@media (max-width: 600px){.detail-value{font-size:13px;word-break:break-all}}.devices-status-card{border:1px solid rgba(0,0,0,.08);border-radius:12px;padding:20px;background:#fff}@media (max-width: 600px){.devices-status-card{padding:16px;border-radius:8px}}.card-title{margin:0 0 20px;font-size:16px;font-weight:600;color:#1a1a1a;text-transform:uppercase;letter-spacing:.5px}@media (max-width: 600px){.card-title{font-size:14px;margin:0 0 16px}}.device-status-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 0}@media (max-width: 600px){.device-status-row{flex-wrap:wrap;gap:12px;padding:12px 0}}.device-info{display:flex;align-items:center;gap:16px;flex:1;min-width:0}@media (max-width: 600px){.device-info{gap:12px;flex:1 1 100%}}.device-icon{font-size:36px;width:36px;height:36px;color:#1976d2;flex-shrink:0}@media (max-width: 600px){.device-icon{font-size:32px;width:32px;height:32px}}.device-details{flex:1;min-width:0}.device-name{font-weight:600;font-size:15px;color:#1a1a1a;margin-bottom:4px}@media (max-width: 600px){.device-name{font-size:14px}}.device-id{font-size:12px;color:#757575;font-family:Courier New,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}@media (max-width: 600px){.device-id{font-size:11px}}.device-status-info{display:flex;flex-direction:column;align-items:flex-end;gap:6px}@media (max-width: 600px){.device-status-info{flex:1 1 100%;flex-direction:row;justify-content:space-between;align-items:center}}.status-badge{display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:20px;font-size:13px;font-weight:600;background:#ffebee;color:#c62828;border:1px solid rgba(198,40,40,.2);box-shadow:0 2px 4px #0000001a}@media (max-width: 600px){.status-badge{font-size:12px;padding:5px 12px}}.status-badge.online{background:#e8f5e9;color:#2e7d32;border:1px solid rgba(46,125,50,.2)}.status-badge mat-icon{font-size:16px;width:16px;height:16px}.last-ping{font-size:12px;color:#9e9e9e;font-weight:500;white-space:nowrap}@media (max-width: 600px){.last-ping{font-size:11px}}mat-divider{margin:12px 0}@media (max-width: 600px){mat-divider{margin:8px 0}}.warning-message{display:flex;align-items:center;gap:12px;padding:14px 16px;background:linear-gradient(135deg,#fff3e0,#ffe0b2);border-left:4px solid #ff9800;border-radius:8px;color:#e65100;font-size:14px;font-weight:500;box-shadow:0 2px 6px #ff980026}@media (max-width: 600px){.warning-message{font-size:13px;padding:12px 14px;gap:10px}}.warning-message mat-icon{font-size:24px;width:24px;height:24px;flex-shrink:0}@media (max-width: 600px){.warning-message mat-icon{font-size:20px;width:20px;height:20px}}.no-connection{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:60px 40px;text-align:center;color:#bdbdbd;background:#fafafa;border-radius:12px;border:2px dashed #e0e0e0}@media (max-width: 600px){.no-connection{padding:40px 20px}}.no-connection mat-icon{font-size:72px;width:72px;height:72px;margin-bottom:20px;opacity:.4}@media (max-width: 600px){.no-connection mat-icon{font-size:56px;width:56px;height:56px;margin-bottom:16px}}.no-connection p{margin:0;font-size:16px;font-weight:500;color:#757575}@media (max-width: 600px){.no-connection p{font-size:14px}}.dialog-actions button{margin-left:8px}.dialog-actions button mat-spinner{display:inline-block;margin-right:8px}\n"] }]
1782
+ }], ctorParameters: () => [{ type: i1$2.MatDialogRef }, { type: undefined, decorators: [{
1783
+ type: Inject,
1784
+ args: [MAT_DIALOG_DATA]
1785
+ }] }, { type: AngularRemoteUploadService }] });
1786
+
1787
+ class AngularCameraCaptureDialogComponent {
1788
+ dialogRef;
1789
+ data;
1790
+ videoElement;
1791
+ canvasElement;
1792
+ currentStream;
1793
+ currentDeviceIndex = 0;
1794
+ isCapturing = signal(false);
1795
+ isSwitching = signal(false);
1796
+ showFlash = signal(false);
1797
+ hasMultipleCameras = false;
1798
+ constructor(dialogRef, data) {
1799
+ this.dialogRef = dialogRef;
1800
+ this.data = data;
1801
+ this.currentStream = data.stream;
1802
+ this.hasMultipleCameras = (data.videoDevices?.length ?? 0) > 1;
1803
+ }
1804
+ ngOnInit() {
1805
+ // Wait for view to initialize before setting video source
1806
+ setTimeout(() => {
1807
+ if (this.videoElement) {
1808
+ this.videoElement.nativeElement.srcObject = this.currentStream;
1809
+ }
1810
+ });
1811
+ }
1812
+ ngOnDestroy() {
1813
+ this.stopStream();
1814
+ }
1815
+ async switchCamera() {
1816
+ if (!this.data.videoDevices || this.data.videoDevices.length <= 1) {
1817
+ return;
1818
+ }
1819
+ this.isSwitching.set(true);
1820
+ try {
1821
+ // Stop current stream
1822
+ this.stopStream();
1823
+ // Move to next camera
1824
+ this.currentDeviceIndex = (this.currentDeviceIndex + 1) % this.data.videoDevices.length;
1825
+ // Get new stream
1826
+ const newStream = await navigator.mediaDevices.getUserMedia({
1827
+ video: {
1828
+ deviceId: this.data.videoDevices[this.currentDeviceIndex].deviceId,
1829
+ width: { ideal: 1920 },
1830
+ height: { ideal: 1080 }
1831
+ }
1832
+ });
1833
+ this.currentStream = newStream;
1834
+ if (this.videoElement) {
1835
+ this.videoElement.nativeElement.srcObject = newStream;
1836
+ }
1837
+ }
1838
+ catch (error) {
1839
+ console.error('Error switching camera:', error);
1840
+ // Could emit an error event here if needed
1841
+ }
1842
+ finally {
1843
+ // Add a small delay to show the switching state
1844
+ setTimeout(() => {
1845
+ this.isSwitching.set(false);
1846
+ }, 500);
1847
+ }
1848
+ }
1849
+ async capturePhoto() {
1850
+ this.isCapturing.set(true);
1851
+ this.showFlash.set(true);
1852
+ // Wait for flash animation
1853
+ setTimeout(async () => {
1854
+ this.showFlash.set(false);
1855
+ const video = this.videoElement.nativeElement;
1856
+ const canvas = this.canvasElement.nativeElement;
1857
+ const ctx = canvas.getContext('2d');
1858
+ if (!ctx) {
1859
+ this.isCapturing.set(false);
1860
+ return;
1861
+ }
1862
+ // Set canvas dimensions to match video
1863
+ canvas.width = video.videoWidth;
1864
+ canvas.height = video.videoHeight;
1865
+ // Draw video frame to canvas
1866
+ ctx.drawImage(video, 0, 0);
1867
+ // Convert canvas to blob
1868
+ canvas.toBlob((blob) => {
1869
+ if (blob) {
1870
+ const file = new File([blob], `camera-capture-${Date.now()}.jpg`, {
1871
+ type: 'image/jpeg',
1872
+ lastModified: Date.now()
1873
+ });
1874
+ const result = {
1875
+ action: 'capture',
1876
+ file
1877
+ };
1878
+ this.dialogRef.close(result);
1879
+ }
1880
+ this.isCapturing.set(false);
1881
+ }, 'image/jpeg', 0.9);
1882
+ }, 300);
1883
+ }
1884
+ openUpload() {
1885
+ const result = {
1886
+ action: 'upload'
1887
+ };
1888
+ this.dialogRef.close(result);
1889
+ }
1890
+ cancel() {
1891
+ const result = {
1892
+ action: 'cancel'
1893
+ };
1894
+ this.dialogRef.close(result);
1895
+ }
1896
+ stopStream() {
1897
+ if (this.currentStream) {
1898
+ this.currentStream.getTracks().forEach(track => track.stop());
1899
+ }
1900
+ }
1901
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularCameraCaptureDialogComponent, deps: [{ token: i1$2.MatDialogRef }, { token: MAT_DIALOG_DATA }], target: i0.ɵɵFactoryTarget.Component });
1902
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: AngularCameraCaptureDialogComponent, isStandalone: true, selector: "angular-camera-capture-dialog", viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoElement"], descendants: true }, { propertyName: "canvasElement", first: true, predicate: ["canvasElement"], descendants: true }], ngImport: i0, template: "<div class=\"camera-modal-overlay\">\n <div class=\"camera-modal-container\">\n <div class=\"video-container\">\n <!-- Video element -->\n <video\n #videoElement\n autoplay\n playsinline\n class=\"camera-video\">\n </video>\n\n <!-- Hidden canvas for capturing -->\n <canvas #canvasElement style=\"display: none;\"></canvas>\n\n <!-- Flash overlay for capture effect -->\n @if (showFlash()) {\n <div class=\"flash-overlay\"></div>\n }\n\n <!-- Camera switch button (only if multiple cameras) -->\n @if (hasMultipleCameras) {\n <button\n mat-mini-fab\n class=\"switch-camera-btn\"\n (click)=\"switchCamera()\"\n [disabled]=\"isSwitching()\"\n aria-label=\"Switch camera\">\n @if (isSwitching()) {\n <mat-spinner diameter=\"20\"></mat-spinner>\n } @else {\n <mat-icon>cameraswitch</mat-icon>\n }\n </button>\n }\n </div>\n\n <!-- Action buttons -->\n <div class=\"action-buttons\">\n <button\n mat-raised-button\n color=\"primary\"\n class=\"action-btn capture-btn\"\n (click)=\"capturePhoto()\"\n [disabled]=\"isCapturing()\">\n <mat-icon>photo_camera</mat-icon>\n @if (!data.isMobile) {\n <span>Capture</span>\n }\n </button>\n\n <button\n mat-raised-button\n color=\"accent\"\n class=\"action-btn upload-btn\"\n (click)=\"openUpload()\">\n <mat-icon>photo_library</mat-icon>\n @if (!data.isMobile) {\n <span>Upload</span>\n }\n </button>\n\n <button\n mat-stroked-button\n class=\"action-btn cancel-btn\"\n (click)=\"cancel()\">\n <mat-icon>close</mat-icon>\n @if (!data.isMobile) {\n <span>Cancel</span>\n }\n </button>\n </div>\n </div>\n</div>\n", styles: [".camera-modal-overlay{position:fixed;inset:0;background:linear-gradient(135deg,#000c,#14141ef2);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:20px;z-index:9999;animation:fadeIn .3s cubic-bezier(.4,0,.2,1)}.camera-modal-container{background:#1a1b1f;border:1px solid rgba(255,255,255,.08);border-radius:20px;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);padding:24px;max-width:800px;width:100%;display:flex;flex-direction:column;gap:20px;animation:slideUp .4s cubic-bezier(.4,0,.2,1)}.video-container{position:relative;width:100%;display:flex;align-items:center;justify-content:center;background:#000;border-radius:12px;overflow:hidden}.camera-video{width:100%;max-width:600px;height:auto;max-height:70vh;border-radius:12px;object-fit:cover;display:block}.flash-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:#fffc;border-radius:12px;animation:flash .3s ease-out;pointer-events:none;z-index:10}.switch-camera-btn{position:absolute;top:16px;right:16px;background:#0009!important;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);z-index:10;transition:all .3s ease}.switch-camera-btn:hover:not([disabled]){background:#000c!important;transform:scale(1.05)}.switch-camera-btn:disabled{opacity:.7}.action-buttons{display:flex;gap:12px;justify-content:center;align-items:center;flex-wrap:wrap}.action-btn{min-width:100px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;border-radius:25px!important;padding:14px 20px!important;display:flex;align-items:center;justify-content:center;gap:8px;transition:all .3s ease}.action-btn mat-icon{font-size:20px;width:20px;height:20px;margin:0}.capture-btn{background:linear-gradient(135deg,#007aff,#0051d5);color:#fff!important;border:none;padding:14px 20px;border-radius:25px;font-size:13px;font-weight:600;cursor:pointer;width:auto;min-width:100px;transition:.3s;display:flex;align-items:center;justify-content:center;text-transform:uppercase;letter-spacing:.5px;box-shadow:#007aff66 0 4px 20px;transform:scale(1)}.capture-btn:hover:not([disabled]){transform:scale(1.05);box-shadow:0 8px 25px #3f51b599}.upload-btn{background:linear-gradient(135deg,#34c759,#248a3d);color:#fff!important;border:none;padding:14px 20px;border-radius:25px;font-size:13px;font-weight:600;cursor:pointer;width:auto;min-width:100px;transition:.3s;display:flex;align-items:center;justify-content:center;text-transform:uppercase;letter-spacing:.5px;box-shadow:#34c75966 0 4px 20px;transform:scale(1)}.upload-btn:hover{transform:scale(1.05);box-shadow:0 8px 25px #ff408199}.cancel-btn{background:#ffffff1a;color:#fff!important;border:2px solid rgba(255,255,255,.3);padding:14px 20px;border-radius:25px;font-size:13px;font-weight:600;cursor:pointer;width:auto;min-width:100px;transition:.3s;display:flex;align-items:center;justify-content:center;text-transform:uppercase;letter-spacing:.5px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);transform:scale(1)}.cancel-btn:hover{background:#fff3!important;transform:scale(1.05)}@media (max-width: 768px){.camera-modal-container{padding:16px}.action-btn{min-width:50px;padding:12px!important}.action-btn span{display:none}.action-btn mat-icon{margin:0}.switch-camera-btn{width:40px;height:40px}}@keyframes fadeIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes flash{0%{opacity:0}50%{opacity:1}to{opacity:0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatDialogModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i4$1.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i4$1.MatMiniFabButton, selector: "button[mat-mini-fab]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i4.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }] });
1903
+ }
1904
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularCameraCaptureDialogComponent, decorators: [{
1905
+ type: Component,
1906
+ args: [{ selector: 'angular-camera-capture-dialog', standalone: true, imports: [
1907
+ CommonModule,
1908
+ MatDialogModule,
1909
+ MatButtonModule,
1910
+ MatIconModule,
1911
+ MatProgressSpinnerModule
1912
+ ], template: "<div class=\"camera-modal-overlay\">\n <div class=\"camera-modal-container\">\n <div class=\"video-container\">\n <!-- Video element -->\n <video\n #videoElement\n autoplay\n playsinline\n class=\"camera-video\">\n </video>\n\n <!-- Hidden canvas for capturing -->\n <canvas #canvasElement style=\"display: none;\"></canvas>\n\n <!-- Flash overlay for capture effect -->\n @if (showFlash()) {\n <div class=\"flash-overlay\"></div>\n }\n\n <!-- Camera switch button (only if multiple cameras) -->\n @if (hasMultipleCameras) {\n <button\n mat-mini-fab\n class=\"switch-camera-btn\"\n (click)=\"switchCamera()\"\n [disabled]=\"isSwitching()\"\n aria-label=\"Switch camera\">\n @if (isSwitching()) {\n <mat-spinner diameter=\"20\"></mat-spinner>\n } @else {\n <mat-icon>cameraswitch</mat-icon>\n }\n </button>\n }\n </div>\n\n <!-- Action buttons -->\n <div class=\"action-buttons\">\n <button\n mat-raised-button\n color=\"primary\"\n class=\"action-btn capture-btn\"\n (click)=\"capturePhoto()\"\n [disabled]=\"isCapturing()\">\n <mat-icon>photo_camera</mat-icon>\n @if (!data.isMobile) {\n <span>Capture</span>\n }\n </button>\n\n <button\n mat-raised-button\n color=\"accent\"\n class=\"action-btn upload-btn\"\n (click)=\"openUpload()\">\n <mat-icon>photo_library</mat-icon>\n @if (!data.isMobile) {\n <span>Upload</span>\n }\n </button>\n\n <button\n mat-stroked-button\n class=\"action-btn cancel-btn\"\n (click)=\"cancel()\">\n <mat-icon>close</mat-icon>\n @if (!data.isMobile) {\n <span>Cancel</span>\n }\n </button>\n </div>\n </div>\n</div>\n", styles: [".camera-modal-overlay{position:fixed;inset:0;background:linear-gradient(135deg,#000c,#14141ef2);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;padding:20px;z-index:9999;animation:fadeIn .3s cubic-bezier(.4,0,.2,1)}.camera-modal-container{background:#1a1b1f;border:1px solid rgba(255,255,255,.08);border-radius:20px;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);padding:24px;max-width:800px;width:100%;display:flex;flex-direction:column;gap:20px;animation:slideUp .4s cubic-bezier(.4,0,.2,1)}.video-container{position:relative;width:100%;display:flex;align-items:center;justify-content:center;background:#000;border-radius:12px;overflow:hidden}.camera-video{width:100%;max-width:600px;height:auto;max-height:70vh;border-radius:12px;object-fit:cover;display:block}.flash-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background:#fffc;border-radius:12px;animation:flash .3s ease-out;pointer-events:none;z-index:10}.switch-camera-btn{position:absolute;top:16px;right:16px;background:#0009!important;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);z-index:10;transition:all .3s ease}.switch-camera-btn:hover:not([disabled]){background:#000c!important;transform:scale(1.05)}.switch-camera-btn:disabled{opacity:.7}.action-buttons{display:flex;gap:12px;justify-content:center;align-items:center;flex-wrap:wrap}.action-btn{min-width:100px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;border-radius:25px!important;padding:14px 20px!important;display:flex;align-items:center;justify-content:center;gap:8px;transition:all .3s ease}.action-btn mat-icon{font-size:20px;width:20px;height:20px;margin:0}.capture-btn{background:linear-gradient(135deg,#007aff,#0051d5);color:#fff!important;border:none;padding:14px 20px;border-radius:25px;font-size:13px;font-weight:600;cursor:pointer;width:auto;min-width:100px;transition:.3s;display:flex;align-items:center;justify-content:center;text-transform:uppercase;letter-spacing:.5px;box-shadow:#007aff66 0 4px 20px;transform:scale(1)}.capture-btn:hover:not([disabled]){transform:scale(1.05);box-shadow:0 8px 25px #3f51b599}.upload-btn{background:linear-gradient(135deg,#34c759,#248a3d);color:#fff!important;border:none;padding:14px 20px;border-radius:25px;font-size:13px;font-weight:600;cursor:pointer;width:auto;min-width:100px;transition:.3s;display:flex;align-items:center;justify-content:center;text-transform:uppercase;letter-spacing:.5px;box-shadow:#34c75966 0 4px 20px;transform:scale(1)}.upload-btn:hover{transform:scale(1.05);box-shadow:0 8px 25px #ff408199}.cancel-btn{background:#ffffff1a;color:#fff!important;border:2px solid rgba(255,255,255,.3);padding:14px 20px;border-radius:25px;font-size:13px;font-weight:600;cursor:pointer;width:auto;min-width:100px;transition:.3s;display:flex;align-items:center;justify-content:center;text-transform:uppercase;letter-spacing:.5px;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);transform:scale(1)}.cancel-btn:hover{background:#fff3!important;transform:scale(1.05)}@media (max-width: 768px){.camera-modal-container{padding:16px}.action-btn{min-width:50px;padding:12px!important}.action-btn span{display:none}.action-btn mat-icon{margin:0}.switch-camera-btn{width:40px;height:40px}}@keyframes fadeIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes flash{0%{opacity:0}50%{opacity:1}to{opacity:0}}\n"] }]
1913
+ }], ctorParameters: () => [{ type: i1$2.MatDialogRef }, { type: undefined, decorators: [{
1914
+ type: Inject,
1915
+ args: [MAT_DIALOG_DATA]
1916
+ }] }], propDecorators: { videoElement: [{
1917
+ type: ViewChild,
1918
+ args: ['videoElement', { static: false }]
1919
+ }], canvasElement: [{
1920
+ type: ViewChild,
1921
+ args: ['canvasElement', { static: false }]
1922
+ }] } });
1923
+
1924
+ const generateRandomString = (length) => {
1925
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1926
+ let result = '';
1927
+ for (let i = 0; i < length; i++) {
1928
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
1929
+ }
1930
+ return result;
1931
+ };
1932
+ class AngularImageAndFilePickerComponent {
1933
+ dialog;
1934
+ helper;
1935
+ breakpointObserver;
1936
+ remoteUploadService;
1937
+ changeSubject = new BehaviorSubject(false);
1938
+ destroy$ = new Subject();
1939
+ urlConfig;
1940
+ entityType;
1941
+ entityId;
1942
+ viewType = 'card';
1943
+ type = 'image';
1944
+ label = null;
1945
+ disabled = false;
1946
+ data;
1947
+ hint = null;
1948
+ accept = '';
1949
+ isValidateMimeType = true;
1950
+ selectedId = null;
1951
+ options = null;
1952
+ required = false;
1953
+ previewOnly = false;
1954
+ previewInFlex = false;
1955
+ imageItemClass = '';
1956
+ isAddUploadedFileInLastNode = false;
1957
+ isEnableDeleteConfirmation = true;
1958
+ isEnableCapture = false;
1959
+ deleteConfirmationMsg;
1960
+ dialogConfig;
1961
+ enableDragNDropForUpload = false;
1962
+ // Remote Upload Configuration
1963
+ remoteUploadConfig = null;
1964
+ uploadInProgress = new EventEmitter();
1965
+ onUploaded = new EventEmitter();
1966
+ onFileSelect = new EventEmitter();
1967
+ onFileRemoved = new EventEmitter();
1968
+ onError = new EventEmitter();
1969
+ onRemoteUpload = new EventEmitter();
1970
+ isShowImageSequence = false;
1971
+ enableDragNDrop = false;
1972
+ editTagButtonWhenDisabled = false;
1973
+ showDeleteButtonWhenDisabled = false;
1974
+ dataSequenceChange = new EventEmitter();
1975
+ isMobile = false;
1976
+ isTab = false;
1977
+ // Mobile upload state
1978
+ isWaitingForMobileUpload = false;
1979
+ mobileUploadFieldLabel = '';
1980
+ // Pending files from mobile (waiting to be accepted/rejected)
1981
+ pendingFiles = [];
1982
+ isHandset$;
1983
+ isTab$;
1984
+ config = {
1985
+ isCompressed: false,
1986
+ hiddenDeleteBtn: false,
1987
+ hiddenPreview: false,
1988
+ selectionMode: false,
1989
+ isStoredDb: false,
1990
+ isMultiple: false,
1991
+ limit: 10,
1992
+ cols: 5,
1993
+ colsForTab: 5,
1994
+ colsForMobile: 3,
1995
+ height: '130px',
1996
+ selectorId: generateRandomString(10),
1997
+ enableImageTags: false,
1998
+ useAdvancedCamera: true // Enable advanced camera modal by default
1999
+ };
2000
+ isSliderLoaded = true;
2001
+ filesArray = [
2002
+ // {
2003
+ // "s3Url": "https://obk-servicemind-uat-resources.s3.ap-southeast-1.amazonaws.com/parcel_create/da226bf2-4e5f-4180-94d8-6746b1ce139b.jpg",
2004
+ // "uploadData": {
2005
+ // "isBase64Encoded": false,
2006
+ // "headers": {
2007
+ // "Access-Control-Allow-Origin": "*"
2008
+ // },
2009
+ // "uploadURL": "https://obk-servicemind-uat-resources.s3.ap-southeast-1.amazonaws.com/parcel_create/da226bf2-4e5f-4180-94d8-6746b1ce139b.jpg?Content-Type=image%2Fjpeg&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA37VN5NQI5HY4VFVN%2F20220617%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20220617T152621Z&X-Amz-Expires=900&X-Amz-Signature=4004cfb10a740b9623d486de992a06f53b458ed6fc151c092eea88a3059eae8f&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read",
2010
+ // "photoFilename": "f7a32cd0-d0c1-474a-838e-47402d80527a.jpeg",
2011
+ // "fileName": "1452664590074.jpeg",
2012
+ // "uploadPath": "/survey_orders/f7a32cd0-d0c1-474a-838e-47402d80527a.jpeg",
2013
+ // "resourceUrl": "https://obk-servicemind-uat-resources.s3.ap-southeast-1.amazonaws.com/parcel_create/da226bf2-4e5f-4180-94d8-6746b1ce139b.jpg"
2014
+ // }
2015
+ // },
2016
+ // {
2017
+ // "s3Url": "https://obk-servicemind-uat-resources.s3.ap-southeast-1.amazonaws.com/parcel_create/31033a2a-5db6-450d-a541-58a94654dd0c.jpg",
2018
+ // "uploadData": {
2019
+ // "isBase64Encoded": false,
2020
+ // "headers": {
2021
+ // "Access-Control-Allow-Origin": "*"
2022
+ // },
2023
+ // "uploadURL": "https://servicemind-resources-staging.s3.ap-southeast-1.amazonaws.com/survey_orders/6c537889-c72a-48ca-9134-638e8d6a1aac.sql?Content-Type=application%2Fsql&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA37VN5NQI5HY4VFVN%2F20220617%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20220617T152641Z&X-Amz-Expires=900&X-Amz-Signature=bcdf347df2a6495615bf48d619492e1deac2264231161da5dc3becaa7f45a796&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read",
2024
+ // "photoFilename": "6c537889-c72a-48ca-9134-638e8d6a1aac.sql",
2025
+ // "fileName": "eccom_bermuda.sql",
2026
+ // "uploadPath": "/survey_orders/6c537889-c72a-48ca-9134-638e8d6a1aac.sql",
2027
+ // "resourceUrl": "https://servicemind-resources-staging.s3.ap-southeast-1.amazonaws.com/survey_orders/6c537889-c72a-48ca-9134-638e8d6a1aac.sql"
2028
+ // }
2029
+ // },
2030
+ // {
2031
+ // "s3Url": "https://obk-servicemind-uat-resources.s3.ap-southeast-1.amazonaws.com/parcel_create/b8424185-30c6-4f44-ba1e-ff616dfd6576.jpg",
2032
+ // "uploadData": {
2033
+ // "isBase64Encoded": false,
2034
+ // "headers": {
2035
+ // "Access-Control-Allow-Origin": "*"
2036
+ // },
2037
+ // "uploadURL": "https://servicemind-resources-staging.s3.ap-southeast-1.amazonaws.com/survey_orders/232f2548-dc93-4204-8835-86b69f3005a7.pdf?Content-Type=application%2Fpdf&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA37VN5NQI5HY4VFVN%2F20220617%2Fap-southeast-1%2Fs3%2Faws4_request&X-Amz-Date=20220617T152656Z&X-Amz-Expires=900&X-Amz-Signature=85643479c3fbecc5bd1fee9783e3cb72e27e16a034921b52cf2f377eefb01ff9&X-Amz-SignedHeaders=host%3Bx-amz-acl&x-amz-acl=public-read",
2038
+ // "photoFilename": "232f2548-dc93-4204-8835-86b69f3005a7.pdf",
2039
+ // "fileName": "print-registration-4x6.pdf",
2040
+ // "uploadPath": "/survey_orders/232f2548-dc93-4204-8835-86b69f3005a7.pdf",
2041
+ // "resourceUrl": "https://servicemind-resources-staging.s3.ap-southeast-1.amazonaws.com/survey_orders/232f2548-dc93-4204-8835-86b69f3005a7.pdf"
2042
+ // }
2043
+ // }
2044
+ ];
2045
+ currentEntityId;
2046
+ currentEntityType;
2047
+ images = [];
2048
+ loading = false;
2049
+ status = false;
2050
+ constructor(dialog, helper, breakpointObserver, remoteUploadService) {
2051
+ this.dialog = dialog;
2052
+ this.helper = helper;
2053
+ this.breakpointObserver = breakpointObserver;
2054
+ this.remoteUploadService = remoteUploadService;
2055
+ }
2056
+ ngOnInit() {
2057
+ this.isHandset$ = this.breakpointObserver.observe([Breakpoints.Handset])
2058
+ .pipe(map(result => result.matches), shareReplay());
2059
+ this.isTab$ = this.breakpointObserver.observe([Breakpoints.TabletPortrait])
2060
+ .pipe(map(result => result.matches), shareReplay());
2061
+ this.prepareConfig();
2062
+ // Initialize remote upload if configured
2063
+ this.initRemoteUpload();
2064
+ // if(this.viewType == 'single-card'){
2065
+ // this.generateFilesForSingleCard();
2066
+ // }
2067
+ }
2068
+ ngAfterViewInit() {
2069
+ this.isHandset$
2070
+ .pipe(takeUntil$1(this.destroy$))
2071
+ .subscribe(r => {
2072
+ console.log('IS HANDSET:', r);
2073
+ this.isMobile = r;
2074
+ this.changeSubject.next(true);
2075
+ });
2076
+ this.isTab$
2077
+ .pipe(takeUntil$1(this.destroy$))
2078
+ .subscribe(r => {
2079
+ console.log('IS TAB:', r);
2080
+ this.isTab = r;
2081
+ this.changeSubject.next(true);
2082
+ });
2083
+ this.changeSubject
2084
+ .pipe(takeUntil$1(this.destroy$))
2085
+ .subscribe(status => {
2086
+ let cols = 1;
2087
+ if (this.isMobile) {
2088
+ cols = this.options?.colsForMobile ?? 3;
2089
+ }
2090
+ else if (this.isTab) {
2091
+ cols = this.options?.colsForTab ?? 4;
2092
+ }
2093
+ else {
2094
+ cols = this.options?.cols ?? 5;
2095
+ }
2096
+ this.config.cols = cols;
2097
+ });
2098
+ }
2099
+ ngOnChanges(changes) {
2100
+ if (changes['entityId']) {
2101
+ this.currentEntityId = changes['entityId'].currentValue;
2102
+ }
2103
+ if (changes['entityType']) {
2104
+ this.currentEntityType = changes['entityType'].currentValue;
2105
+ }
2106
+ if (changes['urlConfig']) {
2107
+ this.urlConfig = changes['urlConfig'].currentValue;
2108
+ this.config.isStoredDb = false;
2109
+ if (this.urlConfig?.attachToEntity) {
2110
+ this.config.isStoredDb = true;
2111
+ }
2112
+ }
2113
+ if (changes['options']) {
2114
+ this.prepareConfig();
2115
+ }
2116
+ if (changes['data']) {
2117
+ const incomingData = changes['data']?.currentValue ?? [];
2118
+ // Safety check: ensure filesArray is initialized
2119
+ if (!this.filesArray) {
2120
+ this.filesArray = [];
2121
+ }
2122
+ // Merge with existing, avoiding duplicates by s3Url
2123
+ const existingUrls = new Set(this.filesArray
2124
+ .filter((f) => f && f.s3Url) // Filter out null/undefined
2125
+ .map((f) => f.s3Url));
2126
+ const newFiles = incomingData.filter((f) => f && f.s3Url && !existingUrls.has(f.s3Url));
2127
+ // If new files exist or filesArray is empty, update with incoming data
2128
+ // If all incoming files already exist, keep the existing filesArray
2129
+ if (newFiles.length > 0 || this.filesArray.length === 0) {
2130
+ this.filesArray = incomingData.map((r) => {
2131
+ if (r) {
2132
+ r.loading = false;
2133
+ }
2134
+ return r;
2135
+ });
2136
+ console.log("this.filesArray", this.filesArray);
2137
+ }
2138
+ else {
2139
+ console.log("this.filesArray - skipped duplicate data update");
2140
+ }
2141
+ }
2142
+ if (changes['viewType'] && changes['viewType'].currentValue == 'compact') {
2143
+ this.config.limit = 1;
2144
+ this.config.isMultiple = false;
2145
+ }
2146
+ if (changes['remoteUploadConfig']) {
2147
+ this.initRemoteUpload();
2148
+ }
2149
+ }
2150
+ prepareConfig() {
2151
+ if (this.options?.isCompressed) {
2152
+ this.config.isCompressed = this.options?.isCompressed;
2153
+ }
2154
+ if (this.options?.isSliderPreview) {
2155
+ this.config.isSliderPreview = this.options?.isSliderPreview;
2156
+ }
2157
+ if (this.options?.hiddenDeleteBtn) {
2158
+ this.config.hiddenDeleteBtn = this.options?.hiddenDeleteBtn;
2159
+ }
2160
+ if (this.options?.hiddenPreview) {
2161
+ this.config.hiddenPreview = this.options?.hiddenPreview;
2162
+ }
2163
+ if (this.options?.selectionMode) {
2164
+ this.config.selectionMode = this.options?.selectionMode;
2165
+ }
2166
+ this.config.isStoredDb = false;
2167
+ if (this.urlConfig?.attachToEntity) {
2168
+ this.config.isStoredDb = true;
2169
+ }
2170
+ if (this.options?.isMultiple) {
2171
+ this.config.isMultiple = this.options?.isMultiple;
2172
+ if (this.viewType == 'compact') {
2173
+ this.config.isMultiple = false;
2174
+ }
2175
+ }
2176
+ if (this.options?.fileSize) {
2177
+ this.config.fileSize = this.options?.fileSize;
2178
+ }
2179
+ if (this.options?.limit) {
2180
+ this.config.limit = this.options?.limit;
2181
+ if (this.viewType == 'compact') {
2182
+ this.config.limit = 1;
2183
+ }
2184
+ }
2185
+ if (this.options?.cols) {
2186
+ this.config.cols = this.options?.cols;
2187
+ }
2188
+ if (this.options?.colsForTab) {
2189
+ this.config.colsForTab = this.options?.colsForTab;
2190
+ }
2191
+ if (this.options?.colsForMobile) {
2192
+ this.config.colsForMobile = this.options?.colsForMobile;
2193
+ }
2194
+ if (this.options?.height) {
2195
+ this.config.height = this.options?.height;
2196
+ }
2197
+ if (this.options?.selectorId) {
2198
+ this.config.selectorId = this.options?.selectorId;
2199
+ }
2200
+ if (this.options?.enableImageTags) {
2201
+ this.config.enableImageTags = this.options?.enableImageTags;
2202
+ }
2203
+ if (this.options?.useAdvancedCamera !== undefined) {
2204
+ this.config.useAdvancedCamera = this.options?.useAdvancedCamera;
2205
+ }
2206
+ }
2207
+ /**
2208
+ * Validates selected files against the accept parameter
2209
+ * @param files - FileList of selected files
2210
+ * @returns boolean - true if all files are valid, false otherwise
2211
+ */
2212
+ validateFileTypes(files) {
2213
+ if (!this.accept || this.accept.trim() === '') {
2214
+ return true; // No validation needed if accept is not specified
2215
+ }
2216
+ const acceptedTypes = this.accept.split(',').map(type => type.trim().toLowerCase());
2217
+ const invalidFiles = [];
2218
+ for (let i = 0; i < files.length; i++) {
2219
+ const file = files[i];
2220
+ const fileExtension = '.' + file.name.split('.').pop()?.toLowerCase();
2221
+ const fileMimeType = file.type.toLowerCase();
2222
+ let isValid = false;
2223
+ for (const acceptType of acceptedTypes) {
2224
+ if (acceptType.startsWith('.')) {
2225
+ // Extension match (e.g., .jpg, .png)
2226
+ if (acceptType === fileExtension) {
2227
+ isValid = true;
2228
+ break;
2229
+ }
2230
+ }
2231
+ else if (acceptType.includes('/*')) {
2232
+ // MIME type wildcard match (e.g., image/*, video/*)
2233
+ const mimePrefix = acceptType.split('/')[0];
2234
+ if (fileMimeType.startsWith(mimePrefix + '/')) {
2235
+ isValid = true;
2236
+ break;
2237
+ }
2238
+ }
2239
+ else if (acceptType.includes('/')) {
2240
+ // Exact MIME type match (e.g., image/jpeg, application/pdf)
2241
+ if (acceptType === fileMimeType) {
2242
+ isValid = true;
2243
+ break;
2244
+ }
2245
+ }
2246
+ }
2247
+ if (!isValid) {
2248
+ invalidFiles.push(file.name);
2249
+ }
2250
+ }
2251
+ if (invalidFiles.length > 0) {
2252
+ const acceptedTypesStr = acceptedTypes.join(', ');
2253
+ const invalidFilesStr = invalidFiles.join(', ');
2254
+ this.helper.showErrorMsg(`Invalid file type(s): ${invalidFilesStr}. Only ${acceptedTypesStr} files are allowed.`, 'Error', 5000);
2255
+ return false;
2256
+ }
2257
+ return true;
2258
+ }
2259
+ setSliderLoading() {
2260
+ this.isSliderLoaded = false;
2261
+ setTimeout(() => {
2262
+ this.isSliderLoaded = true;
2263
+ }, 20);
2264
+ }
2265
+ /**
2266
+ * Handle click on the upload label - only trigger file selection if not clicking on mobile upload buttons
2267
+ */
2268
+ handleLabelClick(event) {
2269
+ if (this.loading) {
2270
+ return;
2271
+ }
2272
+ // Check if the click is on or inside the mobile upload actions container or buttons
2273
+ const target = event.target;
2274
+ const clickedOnMobileActions = target.closest('.mobile-upload-actions');
2275
+ const clickedOnButton = target.closest('button');
2276
+ // If clicked on mobile upload actions or any button, don't trigger file selection
2277
+ if (clickedOnMobileActions || clickedOnButton) {
2278
+ return;
2279
+ }
2280
+ // Otherwise, proceed with normal file selection behavior
2281
+ if (this.type === 'image' && this.isEnableCapture) {
2282
+ this.openCameraCapture();
2283
+ }
2284
+ else {
2285
+ this.openImageSelector();
2286
+ }
2287
+ }
2288
+ openImageSelector(selectorId = this.config?.selectorId) {
2289
+ document.getElementById(selectorId)?.click();
2290
+ }
2291
+ async openCameraCapture() {
2292
+ // Check configuration preference and browser support
2293
+ if (this.config.useAdvancedCamera && typeof navigator !== 'undefined' && navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function') {
2294
+ try {
2295
+ // Try to open camera directly using MediaDevices API
2296
+ await this.openCameraDialog();
2297
+ }
2298
+ catch (error) {
2299
+ console.log('MediaDevices API failed, falling back to file input:', error);
2300
+ // Fallback to file input method
2301
+ this.openImageSelector();
2302
+ }
2303
+ }
2304
+ else {
2305
+ // Use simple file input method (works on most browsers including mobile)
2306
+ this.openImageSelector();
2307
+ }
2308
+ }
2309
+ async openCameraDialog() {
2310
+ try {
2311
+ // Get available cameras first
2312
+ const devices = await navigator.mediaDevices.enumerateDevices();
2313
+ const videoDevices = devices.filter(device => device.kind === 'videoinput');
2314
+ // Request camera access
2315
+ const stream = await navigator.mediaDevices.getUserMedia({
2316
+ video: {
2317
+ facingMode: 'environment', // Prefer rear camera
2318
+ width: { ideal: this.isMobile ? 720 : 1920 },
2319
+ height: { ideal: this.isMobile ? 480 : 1080 }
2320
+ }
2321
+ });
2322
+ // Create a modal dialog with video preview and capture button
2323
+ this.showCameraModal(stream, videoDevices);
2324
+ }
2325
+ catch (error) {
2326
+ console.error('Error accessing camera:', error);
2327
+ if (error.name === 'NotAllowedError') {
2328
+ this.helper.showErrorMsg('Camera access denied. Please allow camera access and try again.', 'Error', 3000);
2329
+ }
2330
+ else if (error.name === 'NotFoundError') {
2331
+ this.helper.showErrorMsg('No camera found. Please use file upload instead.', 'Error', 3000);
2332
+ }
2333
+ else {
2334
+ this.helper.showErrorMsg('Could not access camera. Falling back to file upload.', 'Error', 3000);
2335
+ this.openImageSelector();
2336
+ }
2337
+ throw error;
2338
+ }
2339
+ }
2340
+ showCameraModal(stream, videoDevices) {
2341
+ const dialogData = {
2342
+ stream,
2343
+ videoDevices,
2344
+ isMobile: this.isMobile
2345
+ };
2346
+ const dialogRef = this.dialog.open(AngularCameraCaptureDialogComponent, {
2347
+ data: dialogData,
2348
+ panelClass: 'camera-capture-dialog',
2349
+ maxWidth: '100vw',
2350
+ maxHeight: '100vh',
2351
+ width: '100%',
2352
+ height: '100%',
2353
+ hasBackdrop: true,
2354
+ disableClose: false
2355
+ });
2356
+ dialogRef.afterClosed().subscribe(async (result) => {
2357
+ // Stop stream if dialog was closed
2358
+ stream.getTracks().forEach(track => track.stop());
2359
+ if (!result) {
2360
+ return; // User closed dialog without action
2361
+ }
2362
+ if (result.action === 'capture' && result.file) {
2363
+ // Create a fake event to pass to detectImages
2364
+ const fakeEvent = {
2365
+ target: {
2366
+ files: [result.file],
2367
+ value: ''
2368
+ },
2369
+ preventDefault: () => { }
2370
+ };
2371
+ // Process the captured image
2372
+ await this.detectImages(fakeEvent);
2373
+ }
2374
+ else if (result.action === 'upload') {
2375
+ // Open file selector
2376
+ this.openImageSelector();
2377
+ }
2378
+ // 'cancel' action - do nothing, just close
2379
+ });
2380
+ }
2381
+ async detectImages(event, index = -1) {
2382
+ console.log('detectImages:', event);
2383
+ event.preventDefault();
2384
+ if (this.config?.limit === 1) {
2385
+ this.filesArray = []; // Reset array for a single image
2386
+ }
2387
+ const files = event.target.files;
2388
+ if (!files) {
2389
+ this.helper.showErrorMsg('Please select an image', 'Error', 3000);
2390
+ return;
2391
+ }
2392
+ // Validate file types based on accept parameter
2393
+ if (this.isValidateMimeType && !this.validateFileTypes(files)) {
2394
+ return;
2395
+ }
2396
+ if ((this.filesArray?.length + files.length) > this.config?.limit) {
2397
+ this.helper.showErrorMsg(`You can upload a maximum of ${this.config?.limit} images`, 'Error', 3000);
2398
+ return;
2399
+ }
2400
+ this.loading = true;
2401
+ this.uploadInProgress.emit(true);
2402
+ let uploadedImages = [];
2403
+ // Pre-allocate slots in filesArray to avoid race conditions
2404
+ const startIndex = this.filesArray.length;
2405
+ const placeholders = [...files].map((file, idx) => ({
2406
+ tempId: `temp-${Date.now()}-${idx}`,
2407
+ title: file.name,
2408
+ name: file.name,
2409
+ loading: true,
2410
+ filename: file.name,
2411
+ s3Url: '', // Will be filled during processing
2412
+ sequence: this.isAddUploadedFileInLastNode ? startIndex + idx + 1 : this.filesArray.length + 1 - idx
2413
+ }));
2414
+ // Add all placeholders at once to prevent race conditions
2415
+ if (this.isAddUploadedFileInLastNode) {
2416
+ this.filesArray = [...this.filesArray, ...placeholders];
2417
+ }
2418
+ else {
2419
+ this.filesArray = [...placeholders.reverse(), ...this.filesArray];
2420
+ }
2421
+ this.setSliderLoading();
2422
+ // Process all images concurrently with their pre-assigned indices
2423
+ await Promise.all([...files].map((file, idx) => {
2424
+ const targetIndex = this.isAddUploadedFileInLastNode ? startIndex + idx : idx;
2425
+ return this.processImage(file, uploadedImages, targetIndex, placeholders[this.isAddUploadedFileInLastNode ? idx : files.length - 1 - idx].tempId);
2426
+ }));
2427
+ if (this.config?.isStoredDb) {
2428
+ await this.uploadImages(uploadedImages);
2429
+ }
2430
+ this.loading = false;
2431
+ this.uploadInProgress.emit(false);
2432
+ this.onSubmit(); // Call once after all uploads
2433
+ this.updateSequence();
2434
+ // Reset the input value so the same file can be selected again
2435
+ event.target.value = '';
2436
+ }
2437
+ async processImage(file, uploadedImages, index, tempId) {
2438
+ let fileSize = file.size / 1024;
2439
+ if (this.config?.fileSize && fileSize > this.config?.fileSize) {
2440
+ let maxSize = this.config.fileSize / 1024;
2441
+ this.helper.showErrorMsg(`File size must be ≤ ${maxSize >= 1 ? maxSize : this.config.fileSize} ${maxSize >= 1 ? 'MB' : 'KB'}`, 'Error', 3000);
2442
+ return;
2443
+ }
2444
+ // ponytail: iOS Safari often PUT 0-byte bodies if the original <input> File is
2445
+ // uploaded after FileReader / async gaps. Snapshot bytes into a stable File first.
2446
+ const mimeType = file.type || this.getMimeTypeFromFileName(file.name) || 'image/jpeg';
2447
+ let fileBytes;
2448
+ try {
2449
+ fileBytes = await file.arrayBuffer();
2450
+ }
2451
+ catch (error) {
2452
+ this.helper.showHttpErrorMsg(error);
2453
+ return;
2454
+ }
2455
+ if (!fileBytes || fileBytes.byteLength === 0) {
2456
+ this.helper.showErrorMsg('Unable to read image data. Please try again.', 'Error', 3000);
2457
+ return;
2458
+ }
2459
+ const stableFile = new File([fileBytes], file.name || `image.${this.getExtensionFromMimeType(mimeType)}`, { type: mimeType });
2460
+ let fileToUpload = this.config?.isCompressed
2461
+ ? await this.helper.compressFile(stableFile, mimeType)
2462
+ : stableFile;
2463
+ try {
2464
+ let uploadResponse = await this.helper.getUploadUrl(this.urlConfig.getUploadUrl, stableFile.name, mimeType, this.currentEntityType).toPromise();
2465
+ let uploadData = uploadResponse.data.uploadUrlData;
2466
+ // For initial display, use a data URL for preview but ensure S3 URL is used for database
2467
+ let dataUrl = await this.helper.getDataUrlFromFile(stableFile);
2468
+ let currentImageData = {
2469
+ tempId: tempId || `temp-${Date.now()}`,
2470
+ title: stableFile.name, name: stableFile.name, s3Url: dataUrl, // Temporary preview URL
2471
+ filename: stableFile.name, s3Path: uploadData.uploadPath,
2472
+ tempS3Url: uploadData.resourceUrl, // This will be the final S3 URL
2473
+ id: null,
2474
+ uploadData: uploadData, loading: true, // Set loading to true initially
2475
+ tags: null, tempTags: null, isEditMode: false, sequence: 1,
2476
+ };
2477
+ // Update the file at the pre-assigned index
2478
+ if (index !== -1 && this.filesArray[index]) {
2479
+ this.filesArray[index] = { ...this.filesArray[index], ...currentImageData };
2480
+ }
2481
+ this.setSliderLoading();
2482
+ uploadedImages.push(currentImageData);
2483
+ // Upload to S3 (stable Blob/File — not the original iOS File handle)
2484
+ await this.helper.putFile(uploadData.uploadURL, fileToUpload, mimeType).toPromise();
2485
+ // After successful upload, update with S3 URL and mark as completed
2486
+ if (index !== -1 && this.filesArray[index]) {
2487
+ this.filesArray[index].s3Url = uploadData.resourceUrl; // Use S3 URL
2488
+ this.filesArray[index].loading = false;
2489
+ // Ensure uploadData contains the correct S3 URL for database storage
2490
+ this.filesArray[index].uploadData.resourceUrl = uploadData.resourceUrl;
2491
+ }
2492
+ this.setSliderLoading();
2493
+ }
2494
+ catch (error) {
2495
+ // Set loading to false on error as well
2496
+ if (index !== -1 && this.filesArray[index]) {
2497
+ this.filesArray[index].loading = false;
2498
+ }
2499
+ this.helper.showHttpErrorMsg(error);
2500
+ }
2501
+ }
2502
+ getMimeTypeFromFileName(fileName) {
2503
+ const ext = fileName?.split('.').pop()?.toLowerCase() || '';
2504
+ const map = {
2505
+ jpg: 'image/jpeg',
2506
+ jpeg: 'image/jpeg',
2507
+ png: 'image/png',
2508
+ gif: 'image/gif',
2509
+ webp: 'image/webp',
2510
+ bmp: 'image/bmp',
2511
+ heic: 'image/heic',
2512
+ heif: 'image/heif',
2513
+ };
2514
+ return map[ext] || '';
2515
+ }
2516
+ getExtensionFromMimeType(mimeType) {
2517
+ const map = {
2518
+ 'image/jpeg': 'jpg',
2519
+ 'image/png': 'png',
2520
+ 'image/gif': 'gif',
2521
+ 'image/webp': 'webp',
2522
+ 'image/bmp': 'bmp',
2523
+ 'image/heic': 'heic',
2524
+ 'image/heif': 'heif',
2525
+ };
2526
+ return map[mimeType.toLowerCase()] || 'jpg';
2527
+ }
2528
+ uploadImages(data) {
2529
+ return new Promise((resolve, reject) => {
2530
+ if (data && data?.length > 0) {
2531
+ console.log('There is a image pending to attach:', data);
2532
+ // Keep track of tempIds for matching after API response
2533
+ let tempIdMapping = [];
2534
+ let images = [];
2535
+ let fa = data.map((r) => {
2536
+ if (r?.uploadData) {
2537
+ // Ensure we're using the S3 URL, not base64 data
2538
+ const s3Url = r.uploadData.resourceUrl;
2539
+ // Store tempId separately for matching, don't send it to API
2540
+ tempIdMapping.push({
2541
+ tempId: r.tempId,
2542
+ resourceUrl: s3Url
2543
+ });
2544
+ images.push({
2545
+ fileName: r.uploadData?.fileName,
2546
+ resourceUrl: s3Url, // This ensures S3 URL is stored in database
2547
+ uploadPath: r.uploadData?.uploadPath,
2548
+ uploadURL: r.uploadData?.uploadURL
2549
+ // tempId removed - not sent to API
2550
+ });
2551
+ }
2552
+ return r;
2553
+ });
2554
+ this.helper.attachFilesToEntity(this.urlConfig?.attachToEntity || 'not-specified', { images: images, entityId: this.currentEntityId, entityType: this.currentEntityType }, this.config.limit).subscribe({
2555
+ next: (ir) => {
2556
+ ir?.data?.map((file, idx) => {
2557
+ // Match by tempId first (more reliable for concurrent uploads), then fall back to S3 URL
2558
+ const uploadedImage = tempIdMapping[idx];
2559
+ let selectedIndex = this.filesArray?.findIndex(f => f.tempId === uploadedImage?.tempId);
2560
+ // Fallback to resourceUrl matching if tempId matching fails
2561
+ if (selectedIndex === -1) {
2562
+ selectedIndex = this.filesArray?.findIndex(f => f.uploadData?.resourceUrl == file.s3Url);
2563
+ }
2564
+ if (selectedIndex !== -1) {
2565
+ this.filesArray[selectedIndex] = {
2566
+ ...this.filesArray[selectedIndex],
2567
+ loading: false,
2568
+ id: file?.id || null,
2569
+ s3Url: file.s3Url // Ensure we're using the S3 URL from the response
2570
+ };
2571
+ }
2572
+ });
2573
+ this.onSubmit();
2574
+ resolve(ir);
2575
+ },
2576
+ error: (imErr) => { this.helper.showHttpErrorMsg(imErr); this.onError.emit(true); resolve(false); }
2577
+ });
2578
+ }
2579
+ else {
2580
+ resolve(false);
2581
+ }
2582
+ });
2583
+ }
2584
+ deleteImage(event, t, index, img) {
2585
+ event.stopPropagation();
2586
+ if (this.isEnableDeleteConfirmation) {
2587
+ let confirmBoxData = {
2588
+ title: this.dialogConfig?.title || 'Delete Image',
2589
+ message: this.deleteConfirmationMsg || (this.dialogConfig?.message || 'Are you sure, you want to delete this image?'),
2590
+ iconClass: this.dialogConfig?.iconClass || 'angular-text-danger',
2591
+ icon: this.dialogConfig?.icon || 'delete',
2592
+ approveButtonText: this.dialogConfig?.approveButtonText || 'Yes, delete',
2593
+ approveButtonClass: `angular-approve-button ${this.dialogConfig?.approveButtonClass}`,
2594
+ cancelButtonText: this.dialogConfig?.cancelButtonText || 'No',
2595
+ cancelButtonClass: `angular-cancel-button ${this.dialogConfig?.cancelButtonClass}`
2596
+ };
2597
+ const panelClass = ['angular-confirmation-dialog'];
2598
+ if (this.dialogConfig?.panelClass) {
2599
+ panelClass.push(this.dialogConfig.panelClass);
2600
+ }
2601
+ const dialogRef = this.dialog.open(AngularConfirmationDialogComponent, {
2602
+ width: "450px",
2603
+ panelClass: panelClass,
2604
+ data: confirmBoxData,
2605
+ disableClose: true,
2606
+ });
2607
+ dialogRef.afterClosed().subscribe((result) => {
2608
+ console.log("The dialog was closed with result:", result);
2609
+ if (result) {
2610
+ this.removeImage(event, index, img);
2611
+ }
2612
+ });
2613
+ }
2614
+ else {
2615
+ this.removeImage(event, index, img);
2616
+ }
2617
+ }
2618
+ /*REMOVE SELECTED IMAGE */
2619
+ removeImage(event, index, img) {
2620
+ this.loading = true;
2621
+ this.filesArray[index].loading = true;
2622
+ if (img?.id) {
2623
+ delete img.uploadData;
2624
+ this.removeUploadedImage(img, index, true);
2625
+ }
2626
+ else {
2627
+ this.loading = false;
2628
+ this.filesArray.splice(index, 1);
2629
+ this.setSliderLoading();
2630
+ this.onSubmit();
2631
+ }
2632
+ }
2633
+ removeUploadedImage(img, index, silently = false) {
2634
+ console.log("removeUploadedImage:", img);
2635
+ this.helper.deleteUploadedFile(this.urlConfig.removeImage, img).subscribe((r) => {
2636
+ console.log('Image Delete Res:', r);
2637
+ this.loading = false;
2638
+ this.filesArray.splice(index, 1);
2639
+ this.setSliderLoading();
2640
+ this.onSubmit();
2641
+ if (!silently) {
2642
+ this.helper.showSuccessMsg(r.message, 'Success', 3000);
2643
+ }
2644
+ }, (err) => { this.filesArray[index].loading = false; this.helper.showErrorMsg(err, "Error"); });
2645
+ }
2646
+ async detectFiles(event, index = -1) {
2647
+ console.log('detectFiles:', event);
2648
+ event.preventDefault();
2649
+ if (this.config?.limit === 1) {
2650
+ this.filesArray = []; // Reset array for a single file
2651
+ }
2652
+ const files = event.target.files;
2653
+ if (!files) {
2654
+ this.helper.showErrorMsg('Please select a file', 'Error', 3000);
2655
+ return;
2656
+ }
2657
+ // Validate file types based on accept parameter
2658
+ if (this.isValidateMimeType && !this.validateFileTypes(files)) {
2659
+ return;
2660
+ }
2661
+ if ((this.filesArray?.length + files.length) > this.config?.limit) {
2662
+ this.helper.showErrorMsg(`You can upload a maximum of ${this.config?.limit} files`, 'Error', 3000);
2663
+ return;
2664
+ }
2665
+ this.loading = true;
2666
+ this.uploadInProgress.emit(true);
2667
+ let uploadedFiles = [];
2668
+ // Pre-allocate slots in filesArray to avoid race conditions
2669
+ const startIndex = this.filesArray.length;
2670
+ const placeholders = [...files].map((file, idx) => ({
2671
+ tempId: `temp-${Date.now()}-${idx}`,
2672
+ title: file.name,
2673
+ name: file.name,
2674
+ loading: true,
2675
+ filename: file.name,
2676
+ s3Url: '', // Will be filled during processing
2677
+ sequence: this.isAddUploadedFileInLastNode ? startIndex + idx + 1 : this.filesArray.length + 1 - idx
2678
+ }));
2679
+ // Add all placeholders at once to prevent race conditions
2680
+ if (this.isAddUploadedFileInLastNode) {
2681
+ this.filesArray = [...this.filesArray, ...placeholders];
2682
+ }
2683
+ else {
2684
+ this.filesArray = [...placeholders.reverse(), ...this.filesArray];
2685
+ }
2686
+ this.setSliderLoading();
2687
+ // Process all files concurrently with their pre-assigned indices
2688
+ await Promise.all([...files].map((file, idx) => {
2689
+ const targetIndex = this.isAddUploadedFileInLastNode ? startIndex + idx : idx;
2690
+ return this.processFile(file, uploadedFiles, targetIndex, placeholders[this.isAddUploadedFileInLastNode ? idx : files.length - 1 - idx].tempId);
2691
+ }));
2692
+ if (this.config?.isStoredDb) {
2693
+ await this.uploadFiles(uploadedFiles);
2694
+ }
2695
+ this.loading = false;
2696
+ this.uploadInProgress.emit(false);
2697
+ this.onSubmit(); // Call once after all uploads
2698
+ this.updateSequence();
2699
+ // Reset the input value so the same file can be selected again
2700
+ event.target.value = '';
2701
+ }
2702
+ async processFile(file, uploadedFiles, index, tempId) {
2703
+ let fileSize = file.size / 1024;
2704
+ if (this.config?.fileSize && fileSize > this.config?.fileSize) {
2705
+ let maxSize = this.config.fileSize / 1024;
2706
+ this.helper.showErrorMsg(`File size must be ≤ ${maxSize >= 1 ? maxSize : this.config.fileSize} ${maxSize >= 1 ? 'MB' : 'KB'}`, 'Error', 3000);
2707
+ return;
2708
+ }
2709
+ return new Promise((resolve) => {
2710
+ let reader = new FileReader();
2711
+ reader.onload = async (e) => {
2712
+ let mimeType = file.type;
2713
+ let buffer = new Uint8Array(reader.result);
2714
+ try {
2715
+ let uploadResponse = await this.helper.getUploadUrl(this.urlConfig.getUploadUrl, file.name, mimeType, this.currentEntityType).toPromise();
2716
+ let uploadData = uploadResponse.data.uploadUrlData;
2717
+ let currentFileData = {
2718
+ tempId: tempId || `temp-${Date.now()}`,
2719
+ title: file.name, name: file.name, s3Url: uploadData.resourceUrl, // Use S3 URL
2720
+ s3Path: uploadData.uploadPath, filename: file.name, id: null,
2721
+ uploadData: uploadData, loading: true, buffer: buffer, // Set loading to true initially
2722
+ tags: null, tempTags: null, isEditMode: false, sequence: 1,
2723
+ };
2724
+ // Update the file at the pre-assigned index
2725
+ if (index !== -1 && this.filesArray[index]) {
2726
+ this.filesArray[index] = { ...this.filesArray[index], ...currentFileData };
2727
+ }
2728
+ this.setSliderLoading();
2729
+ uploadedFiles.push(currentFileData);
2730
+ // Upload to S3
2731
+ await this.helper.putFile(uploadData.uploadURL, e.target.result).toPromise();
2732
+ // After successful upload, ensure S3 URL is used and mark as completed
2733
+ if (index !== -1 && this.filesArray[index]) {
2734
+ this.filesArray[index].s3Url = uploadData.resourceUrl; // Ensure S3 URL
2735
+ this.filesArray[index].loading = false;
2736
+ // Ensure uploadData contains the correct S3 URL for database storage
2737
+ this.filesArray[index].uploadData.resourceUrl = uploadData.resourceUrl;
2738
+ }
2739
+ this.setSliderLoading();
2740
+ resolve();
2741
+ }
2742
+ catch (error) {
2743
+ // Set loading to false on error as well
2744
+ if (index !== -1 && this.filesArray[index]) {
2745
+ this.filesArray[index].loading = false;
2746
+ }
2747
+ this.helper.showHttpErrorMsg(error);
2748
+ resolve();
2749
+ }
2750
+ };
2751
+ reader.readAsArrayBuffer(file);
2752
+ });
2753
+ }
2754
+ uploadFiles(data) {
2755
+ return new Promise((resolve, reject) => {
2756
+ if (data && data.length > 0) {
2757
+ console.log('There is a file pending to attach:', data);
2758
+ // Keep track of tempIds for matching after API response
2759
+ let tempIdMapping = [];
2760
+ let files = [];
2761
+ let fa = data.map((r) => {
2762
+ if (r?.uploadData) {
2763
+ // Ensure we're using the S3 URL, not any local file data
2764
+ const s3Url = r.uploadData.resourceUrl;
2765
+ // Store tempId separately for matching, don't send it to API
2766
+ tempIdMapping.push({
2767
+ tempId: r.tempId,
2768
+ resourceUrl: s3Url
2769
+ });
2770
+ files.push({
2771
+ fileName: r.uploadData?.fileName,
2772
+ resourceUrl: s3Url, // This ensures S3 URL is stored in database
2773
+ uploadPath: r.uploadData?.uploadPath,
2774
+ uploadURL: r.uploadData?.uploadURL
2775
+ // tempId removed - not sent to API
2776
+ });
2777
+ }
2778
+ return r;
2779
+ });
2780
+ this.helper.attachFilesToEntity(this.urlConfig?.attachToEntity || 'not-specified', { files: files, entityId: this.currentEntityId, entityType: this.currentEntityType }, this.config.limit).subscribe({
2781
+ next: (ir) => {
2782
+ ir?.data?.map((file, idx) => {
2783
+ // Match by tempId first (more reliable for concurrent uploads), then fall back to S3 URL
2784
+ const uploadedFile = tempIdMapping[idx];
2785
+ let selectedIndex = this.filesArray?.findIndex(f => f.tempId === uploadedFile?.tempId);
2786
+ // Fallback to resourceUrl matching if tempId matching fails
2787
+ if (selectedIndex === -1) {
2788
+ selectedIndex = this.filesArray?.findIndex(f => f.uploadData?.resourceUrl == file.s3Url);
2789
+ }
2790
+ if (selectedIndex !== -1) {
2791
+ this.filesArray[selectedIndex] = {
2792
+ ...this.filesArray[selectedIndex],
2793
+ loading: false,
2794
+ id: file?.id || null,
2795
+ s3Url: file.s3Url // Ensure we're using the S3 URL from the response
2796
+ };
2797
+ }
2798
+ });
2799
+ this.onSubmit();
2800
+ resolve(ir);
2801
+ },
2802
+ error: (imErr) => { this.helper.showHttpErrorMsg(imErr); this.onError.emit(true); resolve(false); }
2803
+ });
2804
+ }
2805
+ else {
2806
+ resolve(false);
2807
+ }
2808
+ });
2809
+ }
2810
+ deleteFile(event, t, index, file) {
2811
+ event.stopPropagation();
2812
+ if (this.isEnableDeleteConfirmation) {
2813
+ let confirmBoxData = {
2814
+ title: this.dialogConfig?.title || 'Delete File',
2815
+ message: this.deleteConfirmationMsg || (this.dialogConfig?.message || 'Are you sure, you want to delete this file?'),
2816
+ iconClass: this.dialogConfig?.iconClass || 'angular-text-danger',
2817
+ icon: this.dialogConfig?.icon || 'delete',
2818
+ approveButtonText: this.dialogConfig?.approveButtonText || 'Yes, delete',
2819
+ approveButtonClass: `angular-approve-button ${this.dialogConfig?.approveButtonClass}`,
2820
+ cancelButtonText: this.dialogConfig?.cancelButtonText || 'No',
2821
+ cancelButtonClass: `angular-cancel-button ${this.dialogConfig?.cancelButtonClass}`
2822
+ };
2823
+ const panelClass = ['angular-confirmation-dialog'];
2824
+ if (this.dialogConfig?.panelClass) {
2825
+ panelClass.push(this.dialogConfig.panelClass);
2826
+ }
2827
+ const dialogRef = this.dialog.open(AngularConfirmationDialogComponent, {
2828
+ width: "450px",
2829
+ panelClass: panelClass,
2830
+ data: confirmBoxData,
2831
+ disableClose: true,
2832
+ });
2833
+ dialogRef.afterClosed().subscribe((result) => {
2834
+ console.log("The dialog was closed with result:", result);
2835
+ if (result) {
2836
+ this.removeFile(event, index, file);
2837
+ }
2838
+ });
2839
+ }
2840
+ else {
2841
+ this.removeFile(event, index, file);
2842
+ }
2843
+ }
2844
+ removeFile(event, index, file) {
2845
+ this.loading = true;
2846
+ this.filesArray[index].loading = true;
2847
+ if (file?.id) {
2848
+ delete file.uploadData;
2849
+ this.removeUploadedFile(file, index, true);
2850
+ }
2851
+ else {
2852
+ this.loading = false;
2853
+ this.filesArray.splice(index, 1);
2854
+ this.setSliderLoading();
2855
+ this.onSubmit();
2856
+ this.onFileRemoved.emit(true);
2857
+ }
2858
+ }
2859
+ removeUploadedFile(file, index, silently = false) {
2860
+ console.log("removeUploadedFile:", file);
2861
+ this.helper.deleteUploadedFile(this.urlConfig.removeImage, file).subscribe((r) => {
2862
+ console.log('File Delete Res:', r);
2863
+ this.loading = false;
2864
+ this.filesArray.splice(index, 1);
2865
+ this.setSliderLoading();
2866
+ this.onSubmit();
2867
+ if (!silently) {
2868
+ this.helper.showSuccessMsg(r.message, 'Success', 3000);
2869
+ }
2870
+ this.onSubmit();
2871
+ }, (err) => { this.filesArray[index].loading = false; this.helper.showErrorMsg(err, "Error"); });
2872
+ }
2873
+ onSubmit() {
2874
+ // this.filesArray = this.filesArray?.map((r, i) => { r.s3Url = r?.uploadData?.resourceUrl; return r; });
2875
+ this.onUploaded.emit(this.filesArray);
2876
+ }
2877
+ async downloadFile(url, fileName) {
2878
+ try {
2879
+ // First try with fetch
2880
+ const response = await fetch(url, {
2881
+ method: 'GET',
2882
+ headers: {
2883
+ 'Content-Type': 'application/octet-stream',
2884
+ },
2885
+ });
2886
+ if (!response.ok) {
2887
+ throw new Error(`HTTP error! status: ${response.status}`);
2888
+ }
2889
+ const blob = await response.blob();
2890
+ // Check if blob has content
2891
+ if (blob.size === 0) {
2892
+ throw new Error('File is empty or could not be fetched');
2893
+ }
2894
+ // Create download link
2895
+ const blobUrl = URL.createObjectURL(blob);
2896
+ const el = document.createElement("a");
2897
+ el.href = blobUrl;
2898
+ el.download = fileName;
2899
+ el.style.display = 'none';
2900
+ // Add to DOM, click, and remove
2901
+ document.body.appendChild(el);
2902
+ el.click();
2903
+ document.body.removeChild(el);
2904
+ // Cleanup blob URL
2905
+ setTimeout(() => {
2906
+ URL.revokeObjectURL(blobUrl);
2907
+ }, 100);
2908
+ }
2909
+ catch (error) {
2910
+ console.error("Fetch download failed, trying alternative method:", error);
2911
+ // Fallback: Try direct link opening
2912
+ try {
2913
+ const el = document.createElement("a");
2914
+ el.href = url;
2915
+ el.download = fileName;
2916
+ el.target = '_blank';
2917
+ el.style.display = 'none';
2918
+ document.body.appendChild(el);
2919
+ el.click();
2920
+ document.body.removeChild(el);
2921
+ }
2922
+ catch (fallbackError) {
2923
+ console.error("All download methods failed:", fallbackError);
2924
+ this.helper.showErrorMsg("Failed to download file. Please try again or contact support.", 'Error', 5000);
2925
+ }
2926
+ }
2927
+ }
2928
+ clickTimeout = null;
2929
+ clickDelay = 250; // Delay in milliseconds to distinguish single and double click
2930
+ onSelectFile(file) {
2931
+ if (this.config.selectionMode) {
2932
+ if (this.clickTimeout) {
2933
+ clearTimeout(this.clickTimeout);
2934
+ this.clickTimeout = null;
2935
+ this.openFile(file); // Double-click detected
2936
+ }
2937
+ else {
2938
+ this.clickTimeout = setTimeout(() => {
2939
+ this.clickTimeout = null;
2940
+ if (this.config.selectionMode) {
2941
+ this.onFileSelect.emit(file); // Single-click detected
2942
+ }
2943
+ }, this.clickDelay);
2944
+ }
2945
+ }
2946
+ else {
2947
+ this.clickTimeout = null;
2948
+ this.openFile(file);
2949
+ }
2950
+ }
2951
+ openFile(file) {
2952
+ if (!this.config.hiddenPreview) {
2953
+ this.openPreviewDialog(file.s3Url, file.title);
2954
+ }
2955
+ }
2956
+ openPreviewDialog(url, title) {
2957
+ if (url.indexOf('.jpg') != -1 || url.indexOf('.jpeg') != -1 || url.indexOf('.png') != -1) {
2958
+ this.openImagePreviewDialog(url);
2959
+ }
2960
+ else if (url.indexOf('.pdf') != -1) {
2961
+ this.openFileViewer(url, 'pdf');
2962
+ }
2963
+ // else if(url.indexOf('.csv') != -1){
2964
+ // this.openFileViewer(url, 'csv');
2965
+ // }
2966
+ else if (url.indexOf('.xlxs') != -1 || url.indexOf('.xlx') != -1 || url.indexOf('.xls') != -1) {
2967
+ this.openFileViewer(url, 'excel');
2968
+ }
2969
+ else {
2970
+ this.downloadFile(url, title);
2971
+ }
2972
+ }
2973
+ openImagePreviewDialog(url) {
2974
+ const dialogRef = this.dialog.open(AngularPreviewImageComponent, {
2975
+ panelClass: ['mat-p-0', 'relative'],
2976
+ data: { url },
2977
+ });
2978
+ dialogRef.afterClosed().subscribe((result) => {
2979
+ console.log('The dialog was closed', result);
2980
+ });
2981
+ }
2982
+ openFileViewer(pdfUrl, fileType) {
2983
+ let dialogData = {
2984
+ src: pdfUrl,
2985
+ fileType: fileType
2986
+ };
2987
+ const dialogRef = this.dialog.open(AngularFileViewerComponent, {
2988
+ width: this.isMobile ? "100%" : "90%",
2989
+ height: this.isMobile ? "100%" : "98%",
2990
+ minHeight: this.isMobile ? "100%" : "80%",
2991
+ maxWidth: this.isMobile ? "100%" : "100%",
2992
+ panelClass: ['angular-file-viewer'],
2993
+ data: dialogData,
2994
+ disableClose: false,
2995
+ });
2996
+ dialogRef.afterClosed().subscribe((result) => {
2997
+ console.log("FileViewer Dialog Closed");
2998
+ });
2999
+ }
3000
+ checkingForPreview(img) {
3001
+ let imageArray = img.split('.');
3002
+ let imageType = imageArray[imageArray?.length - 1];
3003
+ if (imageType == 'png' || imageType == 'jpg' || imageType == 'jpeg') {
3004
+ return true;
3005
+ }
3006
+ else {
3007
+ return false;
3008
+ }
3009
+ }
3010
+ setHeight(id, valueForAddedHeight = 0) {
3011
+ let height = document.getElementById(id)?.offsetWidth;
3012
+ // console.log("=== setHeight::height ===", id, height);
3013
+ return `${(height ?? 0) + valueForAddedHeight}px`;
3014
+ }
3015
+ generateFilesForSingleCard() {
3016
+ if (this.filesArray?.length < this.config.limit) {
3017
+ for (let index = 0; index < (this.config.limit - this.filesArray?.length); index++) {
3018
+ this.filesArray.push({
3019
+ selectorId: generateRandomString(10),
3020
+ tags: null,
3021
+ tempTags: null,
3022
+ isEditMode: false
3023
+ });
3024
+ }
3025
+ }
3026
+ else {
3027
+ this.filesArray = this.filesArray?.map((file) => {
3028
+ return { ...file, selectorId: generateRandomString(10), tempTag: null, isEditMode: false };
3029
+ });
3030
+ }
3031
+ }
3032
+ editTagWithSpace(file) {
3033
+ console.log("=== editTagWithSpace :: file ===", file);
3034
+ let tempTag = file?.tempTags?.trim();
3035
+ console.log("=== editTagWithSpace :: tempTag ===", tempTag);
3036
+ if (tempTag && tempTag != '') {
3037
+ let tempTagsArr = tempTag?.split(' ');
3038
+ console.log("=== editTagWithSpace :: before tempTagsArr ===", tempTagsArr);
3039
+ tempTagsArr = tempTagsArr?.filter(e => (e && e != ''))?.map(t => {
3040
+ if (t.includes('#')) { }
3041
+ else {
3042
+ t = `#${t}`;
3043
+ }
3044
+ return t;
3045
+ });
3046
+ tempTag = tempTagsArr.join(' ');
3047
+ file.tempTags = `${tempTag}`;
3048
+ }
3049
+ else {
3050
+ file.tempTags = tempTag;
3051
+ }
3052
+ }
3053
+ // onKeydown(event: KeyboardEvent, file: any) {
3054
+ // if (event.key === ',' || event.keyCode === 188) {
3055
+ // event.preventDefault();
3056
+ // this.editTagWithComma(file);
3057
+ // }
3058
+ // }
3059
+ // editTagWithComma(file: any){
3060
+ // let tempTag: any = file?.tempTags?.trim();
3061
+ // if(tempTag && tempTag != ''){
3062
+ // let tempTagsArr: any[] = tempTag?.split(' ');
3063
+ // tempTagsArr = tempTagsArr?.filter(e => (e && e != ''))?.map(t =>{
3064
+ // if (t.includes('#')) {}
3065
+ // else{
3066
+ // t = `#${t}`;
3067
+ // }
3068
+ // return t;
3069
+ // });
3070
+ // tempTag = tempTagsArr.join(' ');
3071
+ // tempTag = tempTag.replace(/,/g, '');
3072
+ // file.tempTags = `${tempTag} `;
3073
+ // }
3074
+ // else{
3075
+ // file.tempTags = tempTag
3076
+ // }
3077
+ // }
3078
+ onSubmitTags(file) {
3079
+ file.tags = file?.tempTags?.trim();
3080
+ file.isEditMode = false;
3081
+ this.helper.updateTag(this.urlConfig?.updateTag || 'not-specified', { id: file?.id || null, tag: file?.tags || null }).subscribe({
3082
+ next: (ir) => {
3083
+ this.helper.showSuccessMsg(ir.message, 'Success', 3000);
3084
+ this.onSubmit();
3085
+ },
3086
+ error: (err) => this.helper.showHttpErrorMsg(err)
3087
+ });
3088
+ }
3089
+ // Drag-and-drop upload state
3090
+ isUploadDragOver = false;
3091
+ // Drag state for highlighting
3092
+ isDragging = false;
3093
+ dragSourceIndex = null;
3094
+ dropTargetIndex = null;
3095
+ canAcceptUploadDragDrop() {
3096
+ if (!this.enableDragNDropForUpload || this.disabled || this.previewOnly || this.loading) {
3097
+ return false;
3098
+ }
3099
+ return (this.config?.limit || 0) > (this.filesArray?.length || 0);
3100
+ }
3101
+ onUploadDragOver(event) {
3102
+ if (!this.canAcceptUploadDragDrop()) {
3103
+ return;
3104
+ }
3105
+ event.preventDefault();
3106
+ event.stopPropagation();
3107
+ if (event.dataTransfer) {
3108
+ event.dataTransfer.dropEffect = 'copy';
3109
+ }
3110
+ this.isUploadDragOver = true;
3111
+ }
3112
+ onUploadDragLeave(event) {
3113
+ if (!this.enableDragNDropForUpload) {
3114
+ return;
3115
+ }
3116
+ event.preventDefault();
3117
+ event.stopPropagation();
3118
+ const relatedTarget = event.relatedTarget;
3119
+ const currentTarget = event.currentTarget;
3120
+ if (relatedTarget && currentTarget.contains(relatedTarget)) {
3121
+ return;
3122
+ }
3123
+ this.isUploadDragOver = false;
3124
+ }
3125
+ onUploadDrop(event) {
3126
+ if (!this.canAcceptUploadDragDrop()) {
3127
+ return;
3128
+ }
3129
+ event.preventDefault();
3130
+ event.stopPropagation();
3131
+ this.isUploadDragOver = false;
3132
+ const files = event.dataTransfer?.files;
3133
+ if (!files || files.length === 0) {
3134
+ return;
3135
+ }
3136
+ const syntheticEvent = {
3137
+ preventDefault: () => { },
3138
+ target: { files, value: '' }
3139
+ };
3140
+ if (this.type === 'file') {
3141
+ this.detectFiles(syntheticEvent);
3142
+ }
3143
+ else {
3144
+ this.detectImages(syntheticEvent);
3145
+ }
3146
+ }
3147
+ onDragStarted(index) {
3148
+ this.isDragging = true;
3149
+ this.dragSourceIndex = index;
3150
+ this.dropTargetIndex = null;
3151
+ }
3152
+ onDragEntered(index) {
3153
+ if (this.isDragging && index !== this.dragSourceIndex) {
3154
+ this.dropTargetIndex = index;
3155
+ }
3156
+ }
3157
+ onDragMoved(event) {
3158
+ if (!this.isDragging)
3159
+ return;
3160
+ const pointerPosition = event.pointerPosition;
3161
+ const dropItems = document.querySelectorAll('.drag-drop-item');
3162
+ let foundTarget = null;
3163
+ dropItems.forEach((item) => {
3164
+ const rect = item.getBoundingClientRect();
3165
+ const index = parseInt(item.getAttribute('data-index') || '-1', 10);
3166
+ if (index !== -1 && index !== this.dragSourceIndex) {
3167
+ if (pointerPosition.x >= rect.left &&
3168
+ pointerPosition.x <= rect.right &&
3169
+ pointerPosition.y >= rect.top &&
3170
+ pointerPosition.y <= rect.bottom) {
3171
+ foundTarget = index;
3172
+ }
3173
+ }
3174
+ });
3175
+ this.dropTargetIndex = foundTarget;
3176
+ }
3177
+ onMouseEnterItem(index) {
3178
+ if (this.isDragging && index !== this.dragSourceIndex) {
3179
+ this.dropTargetIndex = index;
3180
+ }
3181
+ }
3182
+ onMouseLeaveItem() {
3183
+ // Only clear if we're dragging - the next mouseenter will set the new target
3184
+ }
3185
+ onDragEnded() {
3186
+ // Don't reset state here - let drop() handle it
3187
+ // This event fires BEFORE drop(), so resetting here clears our tracked indices
3188
+ this.isDragging = false;
3189
+ // Keep dragSourceIndex and dropTargetIndex - they'll be reset in drop()
3190
+ }
3191
+ drop(event) {
3192
+ // Use our tracked source index instead of CDK's previousIndex (which is unreliable for grids)
3193
+ const sourceIndex = this.dragSourceIndex !== null ? this.dragSourceIndex : event.previousIndex;
3194
+ const targetIndex = this.dropTargetIndex !== null ? this.dropTargetIndex : event.currentIndex;
3195
+ // Log drag info for debugging
3196
+ const draggedItem = this.filesArray[sourceIndex];
3197
+ console.log('=== DRAG & DROP DEBUG ===');
3198
+ console.log('Picked element:', {
3199
+ title: draggedItem?.title || draggedItem?.s3Url?.split('/').pop(),
3200
+ originalPosition: sourceIndex + 1,
3201
+ s3Url: draggedItem?.s3Url
3202
+ });
3203
+ console.log('Dropping at position:', targetIndex + 1);
3204
+ console.log('dragSourceIndex (our tracking):', this.dragSourceIndex);
3205
+ console.log('dropTargetIndex (our tracking):', this.dropTargetIndex);
3206
+ console.log('event.currentIndex (CDK):', event.currentIndex);
3207
+ console.log('event.previousIndex (CDK):', event.previousIndex);
3208
+ // Reset drag state
3209
+ this.isDragging = false;
3210
+ this.dragSourceIndex = null;
3211
+ this.dropTargetIndex = null;
3212
+ // Ignore if same position
3213
+ if (sourceIndex === targetIndex) {
3214
+ console.log('Same position - no change needed');
3215
+ return;
3216
+ }
3217
+ // Access current data
3218
+ const currentData = [...this.filesArray];
3219
+ if (!currentData || currentData.length === 0) {
3220
+ return;
3221
+ }
3222
+ // Reorder: move item from source to target position (shift other items)
3223
+ moveItemInArray(currentData, sourceIndex, targetIndex);
3224
+ // Update filesArray with reordered data
3225
+ this.filesArray = currentData;
3226
+ // Log after reorder
3227
+ const newPosition = this.filesArray.findIndex((f) => f.s3Url === draggedItem?.s3Url);
3228
+ console.log('After reorder - element new position:', newPosition + 1);
3229
+ console.log('=========================');
3230
+ this.updateSequence(true);
3231
+ }
3232
+ updateSequence(isShowMessage = false) {
3233
+ // Update sequence for all files (including those without id yet)
3234
+ // Re-calculate sequence based on current array order
3235
+ this.filesArray = this.filesArray.map((file, index) => {
3236
+ return { ...file, sequence: (index + 1) };
3237
+ });
3238
+ if (this.enableDragNDrop && this.urlConfig?.updateSequence) {
3239
+ // Only send files with id to the server for sequence update
3240
+ let filesWithId = this.filesArray.filter(f => f?.id && f?.id != null && f?.id != '');
3241
+ let files = filesWithId?.length ? JSON.parse(JSON.stringify(filesWithId)) : [];
3242
+ files = files?.map(f => {
3243
+ return { id: f?.id, sequence: f?.sequence || 1 };
3244
+ });
3245
+ if (files.length > 0) {
3246
+ this.helper.updateSequence(this.urlConfig?.updateSequence || 'not-specified', { files }).subscribe({
3247
+ next: (ir) => {
3248
+ if (isShowMessage) {
3249
+ this.helper.showSuccessMsg(ir.message, 'Success', 3000);
3250
+ }
3251
+ else {
3252
+ // Only show success message when files actually have IDs (meaning they were uploaded)
3253
+ if (filesWithId.length > 0) {
3254
+ this.helper.showSuccessMsg(`${this.type == 'image' ? 'Image' : 'File'} has been uploaded successfully`, 'Success', 3000);
3255
+ }
3256
+ }
3257
+ this.dataSequenceChange.emit(this.filesArray);
3258
+ },
3259
+ error: (err) => { this.helper.showHttpErrorMsg(err); this.onError.emit(true); }
3260
+ });
3261
+ }
3262
+ else {
3263
+ // If no files with id yet, just emit the current array
3264
+ this.dataSequenceChange.emit(this.filesArray);
3265
+ }
3266
+ }
3267
+ else {
3268
+ this.dataSequenceChange.emit(this.filesArray);
3269
+ }
3270
+ }
3271
+ getTagsArray(tags) {
3272
+ let tempTag = tags?.trim();
3273
+ let tempTagsArr = tempTag?.split(' ');
3274
+ return tempTagsArr?.filter(e => e && e != '');
3275
+ }
3276
+ // =====================
3277
+ // Remote Upload Methods
3278
+ // =====================
3279
+ /**
3280
+ * Check if remote upload is available and configured
3281
+ */
3282
+ isRemoteUploadAvailable() {
3283
+ return !!(this.remoteUploadConfig?.enabled &&
3284
+ this.remoteUploadConfig?.socketAdapter &&
3285
+ !this.isMobile &&
3286
+ !this.isTab);
3287
+ }
3288
+ /**
3289
+ * Initialize remote upload configuration
3290
+ * Call this in ngOnInit or when config changes
3291
+ */
3292
+ initRemoteUpload() {
3293
+ if (this.remoteUploadConfig?.enabled && this.remoteUploadConfig?.socketAdapter) {
3294
+ this.remoteUploadService.configure(this.remoteUploadConfig);
3295
+ // Subscribe to remote uploads (when files are accepted)
3296
+ this.remoteUploadService.getRemoteUploads()
3297
+ .pipe(takeUntil$1(this.destroy$))
3298
+ .subscribe(event => {
3299
+ this.handleRemoteUpload(event);
3300
+ });
3301
+ // Subscribe to pending files (files from mobile waiting to be accepted)
3302
+ this.remoteUploadService.getPendingFiles()
3303
+ .pipe(takeUntil$1(this.destroy$))
3304
+ .subscribe(files => {
3305
+ this.pendingFiles = files;
3306
+ // Reset waiting state when we have pending files
3307
+ if (files.length > 0) {
3308
+ this.isWaitingForMobileUpload = false;
3309
+ }
3310
+ });
3311
+ // Subscribe to waiting state
3312
+ this.remoteUploadService.getIsWaitingForUpload()
3313
+ .pipe(takeUntil$1(this.destroy$))
3314
+ .subscribe(waiting => {
3315
+ this.isWaitingForMobileUpload = waiting;
3316
+ });
3317
+ }
3318
+ }
3319
+ /**
3320
+ * Handle a remote upload event
3321
+ */
3322
+ handleRemoteUpload(event) {
3323
+ console.log('[AngularImageAndFilePicker] Remote upload received:', event);
3324
+ // Reset waiting state
3325
+ this.isWaitingForMobileUpload = false;
3326
+ // Check if file already exists (prevent duplicates)
3327
+ const fileExists = this.filesArray.some((f) => f.s3Url === event.file.s3Url);
3328
+ if (fileExists) {
3329
+ console.log('[AngularImageAndFilePicker] File already exists, skipping:', event.file.fileName);
3330
+ return;
3331
+ }
3332
+ // Check if we've reached the limit
3333
+ if (this.config?.limit && this.filesArray.length >= this.config.limit) {
3334
+ this.helper.showErrorMsg(`Maximum limit of ${this.config.limit} files reached.`, 'Limit Reached');
3335
+ return;
3336
+ }
3337
+ // Add the uploaded file to the array
3338
+ const newFile = {
3339
+ s3Url: event.file.s3Url,
3340
+ title: event.file.fileName,
3341
+ name: event.file.fileName,
3342
+ filename: event.file.fileName,
3343
+ mimeType: event.file.mimeType,
3344
+ size: event.file.size,
3345
+ uploadData: event.file.uploadData,
3346
+ loading: false,
3347
+ sequence: this.filesArray.length + 1,
3348
+ remoteUpload: true,
3349
+ mobileDeviceId: event.mobileDeviceId,
3350
+ uploadedAt: event.timestamp
3351
+ };
3352
+ if (this.isAddUploadedFileInLastNode) {
3353
+ this.filesArray.push(newFile);
3354
+ }
3355
+ else {
3356
+ this.filesArray.unshift(newFile);
3357
+ }
3358
+ this.setSliderLoading();
3359
+ this.onSubmit();
3360
+ this.updateSequence();
3361
+ this.onRemoteUpload.emit(event);
3362
+ this.helper.showSuccessMsg(`${this.type === 'image' ? 'Image' : 'File'} uploaded from mobile device`, 'Remote Upload', 3000);
3363
+ }
3364
+ /**
3365
+ * Open the QR code dialog for mobile upload
3366
+ */
3367
+ openRemoteUploadDialog() {
3368
+ if (!this.isRemoteUploadAvailable()) {
3369
+ this.helper.showErrorMsg('Remote upload is not available', 'Not Available');
3370
+ return;
3371
+ }
3372
+ const dialogData = {
3373
+ title: 'Upload from Mobile',
3374
+ subtitle: `Scan this QR code to upload ${this.type === 'image' ? 'images' : 'files'} from your mobile device`,
3375
+ qrSize: 200,
3376
+ showCountdown: true,
3377
+ fieldInfo: {
3378
+ label: this.label || `${this.type === 'image' ? 'Images' : 'Files'}`,
3379
+ accept: this.accept || (this.type === 'image' ? 'image/*' : '*'),
3380
+ type: this.type,
3381
+ entityType: this.entityType,
3382
+ entityId: this.entityId,
3383
+ isMultiple: this.config.isMultiple,
3384
+ limit: this.config.limit,
3385
+ isCompressed: this.config.isCompressed
3386
+ }
3387
+ };
3388
+ const dialogRef = this.dialog.open(AngularQrCodeDialogComponent, {
3389
+ width: this.isMobile ? '100%' : '470px',
3390
+ maxWidth: '100%',
3391
+ panelClass: ['angular-qr-code-dialog'],
3392
+ data: dialogData,
3393
+ disableClose: false
3394
+ });
3395
+ dialogRef.afterClosed().subscribe((result) => {
3396
+ console.log('[AngularImageAndFilePicker] QR dialog closed:', result);
3397
+ // Files are already added via the remote upload subscription
3398
+ });
3399
+ }
3400
+ /**
3401
+ * Check if currently paired with a mobile device
3402
+ */
3403
+ isRemotePaired() {
3404
+ return this.remoteUploadService.isPaired();
3405
+ }
3406
+ /**
3407
+ * Disconnect from paired mobile device
3408
+ */
3409
+ async disconnectRemote() {
3410
+ await this.remoteUploadService.disconnect();
3411
+ this.isWaitingForMobileUpload = false;
3412
+ this.mobileUploadFieldLabel = '';
3413
+ }
3414
+ /**
3415
+ * Trigger upload from mobile - sends field request to mobile app
3416
+ */
3417
+ async triggerMobileUpload() {
3418
+ if (!this.isRemotePaired()) {
3419
+ // Not connected - open QR dialog to connect first
3420
+ this.openRemoteUploadDialog();
3421
+ return;
3422
+ }
3423
+ // Already connected - send field request to mobile
3424
+ const fieldInfo = {
3425
+ label: this.label || `${this.type === 'image' ? 'Upload Image' : 'Upload File'}`,
3426
+ accept: this.accept || (this.type === 'image' ? 'image/*' : '*'),
3427
+ type: this.type,
3428
+ entityType: this.entityType,
3429
+ entityId: this.entityId,
3430
+ isMultiple: this.config.isMultiple,
3431
+ limit: this.config.limit,
3432
+ remainingSlots: (this.config.limit || 10) - this.filesArray.length,
3433
+ isCompressed: this.config.isCompressed
3434
+ };
3435
+ this.isWaitingForMobileUpload = true;
3436
+ this.mobileUploadFieldLabel = fieldInfo.label;
3437
+ await this.remoteUploadService.sendFieldRequest(fieldInfo);
3438
+ }
3439
+ /**
3440
+ * Cancel waiting for mobile upload
3441
+ */
3442
+ async cancelMobileUpload() {
3443
+ this.isWaitingForMobileUpload = false;
3444
+ await this.remoteUploadService.cancelFieldRequest();
3445
+ }
3446
+ /**
3447
+ * Accept a pending file from mobile upload
3448
+ * This calls the onFileAccept callback if provided, then handles the file
3449
+ */
3450
+ acceptPendingFile(pendingFile) {
3451
+ // Call the user-provided callback if available
3452
+ if (this.remoteUploadConfig?.onFileAccept) {
3453
+ this.remoteUploadConfig.onFileAccept(pendingFile.file);
3454
+ }
3455
+ // Accept the file in the service (removes from pending, emits to remoteUpload$)
3456
+ this.remoteUploadService.acceptPendingFile(pendingFile);
3457
+ }
3458
+ /**
3459
+ * Reject a pending file from mobile upload
3460
+ */
3461
+ rejectPendingFile(pendingFile) {
3462
+ this.remoteUploadService.rejectPendingFile(pendingFile);
3463
+ }
3464
+ /**
3465
+ * Open dialog showing connection status with disconnect option
3466
+ */
3467
+ openViewConnectionDialog() {
3468
+ const dialogRef = this.dialog.open(AngularViewConnectionDialogComponent, {
3469
+ width: '600px',
3470
+ maxWidth: '90vw',
3471
+ data: {
3472
+ title: 'Mobile Connection'
3473
+ },
3474
+ disableClose: false
3475
+ });
3476
+ dialogRef.afterClosed().subscribe(result => {
3477
+ if (result === 'disconnected') {
3478
+ console.log('[AngularImageAndFilePicker] Mobile device disconnected');
3479
+ }
3480
+ });
3481
+ }
3482
+ /**
3483
+ * Check if a MIME type is an image type
3484
+ */
3485
+ isImageMimeType(mimeType) {
3486
+ if (!mimeType)
3487
+ return false;
3488
+ return mimeType.startsWith('image/');
3489
+ }
3490
+ ngOnDestroy() {
3491
+ this.destroy$.next();
3492
+ this.destroy$.complete();
3493
+ }
3494
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerComponent, deps: [{ token: i1$2.MatDialog }, { token: AngularHelperService }, { token: i3$4.BreakpointObserver }, { token: AngularRemoteUploadService }], target: i0.ɵɵFactoryTarget.Component });
3495
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: AngularImageAndFilePickerComponent, isStandalone: false, selector: "angular-image-and-file-picker", inputs: { urlConfig: "urlConfig", entityType: "entityType", entityId: "entityId", viewType: "viewType", type: "type", label: "label", disabled: "disabled", data: "data", hint: "hint", accept: "accept", isValidateMimeType: "isValidateMimeType", selectedId: "selectedId", options: "options", required: "required", previewOnly: "previewOnly", previewInFlex: "previewInFlex", imageItemClass: "imageItemClass", isAddUploadedFileInLastNode: "isAddUploadedFileInLastNode", isEnableDeleteConfirmation: "isEnableDeleteConfirmation", isEnableCapture: "isEnableCapture", deleteConfirmationMsg: "deleteConfirmationMsg", dialogConfig: "dialogConfig", enableDragNDropForUpload: "enableDragNDropForUpload", remoteUploadConfig: "remoteUploadConfig", isShowImageSequence: "isShowImageSequence", enableDragNDrop: "enableDragNDrop", editTagButtonWhenDisabled: "editTagButtonWhenDisabled", showDeleteButtonWhenDisabled: "showDeleteButtonWhenDisabled" }, outputs: { uploadInProgress: "uploadInProgress", onUploaded: "onUploaded", onFileSelect: "onFileSelect", onFileRemoved: "onFileRemoved", onError: "onError", onRemoteUpload: "onRemoteUpload", dataSequenceChange: "dataSequenceChange" }, usesOnChanges: true, ngImport: i0, template: "@if (config) {\n <input type=\"file\" size=\"60\" id=\"{{config.selectorId}}\" (change)=\"type == 'file'? detectFiles($event): detectImages($event)\" accept=\"{{accept}}\" [multiple]=\"config.isMultiple\" style=\"display: none;\" />\n @if(viewType == 'card'){\n <div [class.image-picker-label]=\"!previewOnly\" [class.error]=\"required && (filesArray?.length || 0) <= 0\" [class.upload-drag-over]=\"enableDragNDropForUpload && isUploadDragOver\" [style.height]=\"filesArray?.length ? null : (remoteUploadConfig?.enabled ? ('calc(' + config?.height + ' + 40px)') : config?.height)\" style=\"--angular-image-picker-height: {{config?.height ?? 0}}; --angular-image-upload-config-height: {{remoteUploadConfig?.enabled ? '40px' : '0px'}}\" cdkDropList (cdkDropListDropped)=\"drop($event)\" cdkDropListOrientation=\"horizontal\" (dragover)=\"onUploadDragOver($event)\" (dragleave)=\"onUploadDragLeave($event)\" (drop)=\"onUploadDrop($event)\">\n @if(filesArray?.length){\n <div [ngClass]=\"{'grid-cols-1': config?.cols == 1, 'grid-cols-2': config?.cols == 2, 'grid-cols-3': config?.cols == 3, 'grid-cols-4': config?.cols == 4, 'grid-cols-5': config?.cols == 5, 'grid-cols-6': config?.cols == 6}\" class=\"{{previewInFlex ? 'flex' : 'grid'}} image-list\" style=\"gap: 25px;\">\n @for(file of filesArray; track file.s3Url; let i = $index; let l = $last;){\n @if(enableDragNDrop){\n <div class=\"flex flex-col drag-drop-item\" [attr.data-index]=\"i\" [class.drag-over-highlight]=\"dropTargetIndex === i && isDragging && dragSourceIndex !== i\">\n @if(isShowImageSequence){\n <div class=\"image-description\" style=\"margin-bottom: 15px;\">\n <div class=\"image-description-text\" style=\"text-align: center; font-weight: bold;\">Image {{i + 1}}</div>\n </div>\n }\n <div cdkDrag (cdkDragStarted)=\"onDragStarted(i)\" (cdkDragEnded)=\"onDragEnded()\" (cdkDragMoved)=\"onDragMoved($event)\">\n <div class=\"image-item {{imageItemClass}}\" [class.selected]=\"config?.selectionMode ? selectedId == file.id : false\" [class.angular-curser-pointer]=\"!config?.hiddenPreview || config?.selectionMode\" id=\"image-item-{{config.selectorId}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i)\" [style.background-image]=\"type == 'file'? '' : 'url('+file.s3Url+')'\" (click)=\"onSelectFile(file)\" cdkDragHandle>\n <mat-icon *ngIf=\"type == 'file'\" class=\"active angular-mat-icon-2x\">description</mat-icon>\n <span class=\"material-icons cancel\" (click)=\"type == 'file'? deleteFile($event, {}, i, file) : deleteImage($event, {}, i, file)\" *ngIf=\"(!disabled || showDeleteButtonWhenDisabled) && !config.hiddenDeleteBtn && !file?.loading\">highlight_off</span>\n <span class=\"material-icons download\" (click)=\"$event.stopPropagation(); downloadFile(file.s3Url, file.title)\" *ngIf=\"!config.hiddenDownloadBtn && !file?.loading\">download_for_offline</span>\n \n <!-- Loading overlay -->\n <div class=\"loading-img\" *ngIf=\"file?.loading\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"30\"></mat-progress-spinner>\n </div>\n </div>\n </div>\n @if(config?.enableImageTags){\n <div class=\"image-description\">\n <div class=\"image-description-text\" *ngIf=\"!file?.isEditMode\">\n @if(file?.tags && file?.tags != ''){\n <span class=\"anchor\" *ngFor=\"let tag of getTagsArray(file?.tags)\">{{tag}} </span>\n }\n @else {\n <span>No Tags</span>\n }\n </div>\n <span class=\"material-icons edit-description-btn edit\" style=\"font-size: 20px;\" (click)=\"$event.stopPropagation(); file.tempTags = file.tags; file.isEditMode = true;\" *ngIf=\"!file?.isEditMode && (editTagButtonWhenDisabled || !disabled)\">edit</span>\n <div class=\"image-description-edit\" *ngIf=\"file?.isEditMode\">\n <!-- (keydown)=\"onKeydown($event, file)\" -->\n <input [(ngModel)]=\"file.tempTags\" placeholder=\"Enter tags here...\" (keydown.enter)=\"onSubmitTags(file)\" (keydown.space)=\"editTagWithSpace(file)\" />\n <div class=\"description-actions\">\n <button class=\"description-action-btn description-cancel-btn\" (click)=\"$event.stopPropagation(); file.tempTags = file.tags; file.isEditMode = false;\">Cancel</button>\n <button class=\"description-action-btn description-save-btn\" (click)=\"$event.stopPropagation(); onSubmitTags(file);\">Save</button>\n </div>\n </div>\n </div>\n }\n </div>\n }\n @else {\n <div class=\"flex flex-col\">\n @if(isShowImageSequence){\n <div class=\"image-description\" style=\"margin-bottom: 15px;\">\n <div class=\"image-description-text\" style=\"text-align: center; font-weight: bold;\">Image {{i + 1}}</div>\n </div>\n }\n <div class=\"image-item {{imageItemClass}}\" [class.selected]=\"config?.selectionMode ? selectedId == file.id : false\" [class.angular-curser-pointer]=\"!config?.hiddenPreview || config?.selectionMode\" id=\"image-item-{{config.selectorId}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i)\" [style.background-image]=\"type == 'file'? '' : 'url('+file.s3Url+')'\" (click)=\"onSelectFile(file)\">\n <mat-icon *ngIf=\"type == 'file'\" class=\"active angular-mat-icon-2x\">description</mat-icon>\n <span class=\"material-icons cancel\" (click)=\"type == 'file'? deleteFile($event, {}, i, file) : deleteImage($event, {}, i, file)\" *ngIf=\"(!disabled || showDeleteButtonWhenDisabled) && !config.hiddenDeleteBtn && !file?.loading\">highlight_off</span>\n <span class=\"material-icons download\" (click)=\"$event.stopPropagation(); downloadFile(file.s3Url, file.title)\" *ngIf=\"!config.hiddenDownloadBtn && !file?.loading\">download_for_offline</span>\n \n <!-- Loading overlay -->\n <div class=\"loading-img\" *ngIf=\"file?.loading\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"30\"></mat-progress-spinner>\n </div>\n </div>\n @if(config?.enableImageTags){\n <div class=\"image-description\">\n <div class=\"image-description-text\" *ngIf=\"!file?.isEditMode\">{{file?.tags || 'No Tags'}}</div>\n <span class=\"material-icons edit-description-btn edit\" style=\"font-size: 20px;\" (click)=\"$event.stopPropagation(); file.isEditMode = true;\" *ngIf=\"!file?.isEditMode && !disabled\">edit</span>\n <div class=\"image-description-edit\" *ngIf=\"file?.isEditMode && !disabled\">\n <!-- (keydown)=\"onKeydown($event, file)\" -->\n <input [(ngModel)]=\"file.tempTags\" placeholder=\"Enter tags here...\" (keydown.enter)=\"onSubmitTags(file)\" (keydown.space)=\"editTagWithSpace(file)\" />\n <div class=\"description-actions\">\n <button class=\"description-action-btn description-cancel-btn\" (click)=\"$event.stopPropagation(); file.tempTags = file.tags; file.isEditMode = false;\">Cancel</button>\n <button class=\"description-action-btn description-save-btn\" (click)=\"$event.stopPropagation(); onSubmitTags(file);\">Save</button>\n </div>\n </div>\n </div>\n }\n </div>\n }\n @if(l && (config?.limit || 0) > (filesArray?.length || 0) && !disabled){\n <div class=\"flex flex-col\" style=\"position: relative;\">\n @if(isShowImageSequence){\n <div style=\"margin-top: .5rem; margin-bottom: 15px; height: 35px;\">\n </div>\n }\n \n <!-- Image upload options in grid -->\n <div class=\"image-item uploader\" id=\"image-item-{{config.selectorId}}-{{i}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i+'-'+i, -4)\" style=\"display: flex; flex-direction: column; gap: 8px; justify-content: center; align-items: center; cursor: pointer; position: relative;\" (click)=\"loading ? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n @if(isWaitingForMobileUpload) {\n <!-- Waiting for mobile upload state -->\n <div class=\"mobile-waiting-state hover-section\" style=\"gap: 4px; padding: 8px;\" (click)=\"$event.stopPropagation()\">\n <mat-progress-spinner diameter=\"18\" mode=\"indeterminate\"></mat-progress-spinner>\n <span class=\"waiting-text\" style=\"font-size: 10px; text-align: center;\">Waiting for mobile...</span>\n <div class=\"w-full h-full absolute hover-section-overlay\" style=\"top: 0; left: 0; background-color: #fafafa; justify-content: center; align-items: center;\">\n <button type=\"button\" class=\"cancel-mobile-btn\" style=\"position: unset;\" (click)=\"cancelMobileUpload(); $event.stopPropagation(); $event.preventDefault()\" title=\"Cancel\">\n <mat-icon style=\"display: unset; color: var(--angular-cancel, #bb333b);\">close</mat-icon>\n </button>\n </div>\n </div>\n } @else if(pendingFiles.length > 0) {\n <!-- Pending files from mobile - show accept/reject UI -->\n <div class=\"pending-files-container\" (click)=\"$event.stopPropagation()\">\n <div class=\"pending-files-header\">\n <mat-icon>smartphone</mat-icon>\n <span>{{ pendingFiles.length }} file(s) from mobile</span>\n </div>\n <div class=\"pending-files-list\">\n @for(pf of pendingFiles; track pf.timestamp; let idx = $index) {\n <div class=\"pending-file-item\">\n <div class=\"pending-file-preview\" [style.background-image]=\"isImageMimeType(pf.file.mimeType) ? 'url(' + pf.file.s3Url + ')' : ''\">\n @if(!isImageMimeType(pf.file.mimeType)) {\n <mat-icon>description</mat-icon>\n }\n </div>\n <span class=\"pending-file-name\" [title]=\"pf.file.fileName\">{{ pf.file.fileName }}</span>\n <div class=\"pending-file-actions\">\n <button type=\"button\" mat-icon-button class=\"accept-btn\" (click)=\"acceptPendingFile(pf); $event.stopPropagation()\" title=\"Accept\">\n <mat-icon>check</mat-icon>\n </button>\n <button type=\"button\" mat-icon-button class=\"reject-btn\" (click)=\"rejectPendingFile(pf); $event.stopPropagation()\" title=\"Reject\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n } @else{\n <div class=\"w-full h-full\" style=\"display: flex; flex-direction: column; gap: 8px; justify-content: center; align-items: center;\">\n <mat-icon class=\"active\">upload_file</mat-icon>\n <span style=\"font-weight: 500; font-size: 9px;\">{{label}}</span>\n </div>\n }\n </div>\n </div>\n\n @if(isRemoteUploadAvailable() && !isWaitingForMobileUpload && pendingFiles.length === 0){\n <div class=\"flex flex-col\" style=\"position: relative;\">\n @if(isShowImageSequence){\n <div style=\"margin-top: .5rem; margin-bottom: 15px; height: 35px;\">\n </div>\n }\n \n <div class=\"image-item uploader\" id=\"image-item-{{config.selectorId}}-{{i}}-{{i}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i)\" style=\"display: flex; flex-direction: column; gap: 8px; justify-content: center; align-items: center; cursor: pointer; position: relative; border-width: 0px !important;\">\n <!-- Mobile Upload Section - Positioned at top-right of upload item -->\n @if(isRemotePaired()){\n <span class=\"material-icons cancel\" style=\"color: #4caf50;\" (click)=\"openViewConnectionDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"View Connection\">link</span>\n }\n <div class=\"mobile-upload-overlay-grid w-full h-full\">\n @if(isRemotePaired()){\n <!-- Connected state -->\n <div class=\"mobile-upload-actions w-full h-full\">\n <button type=\"button\" class=\"mobile-upload-btn connected compact\" style=\"width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;\" (click)=\"triggerMobileUpload(); $event.preventDefault(); $event.stopPropagation()\" title=\"Upload from Mobile\">\n <mat-icon style=\"display: unset !important;\">mobile_screen_share</mat-icon>\n <span style=\"font-weight: 500; font-size: 9px;\">Mobile Upload</span>\n </button>\n <!-- <button type=\"button\" class=\"view-connection-btn compact\" (click)=\"openViewConnectionDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"View Connection\">\n <mat-icon style=\"display: unset !important;\">link</mat-icon>\n </button> -->\n </div>\n } @else {\n <!-- Not connected -->\n <div class=\"mobile-upload-actions w-full h-full\">\n <button type=\"button\" class=\"mobile-upload-btn compact\" style=\"width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;\" (click)=\"openRemoteUploadDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"Connect Mobile Device\">\n <mat-icon style=\"display: unset !important;\">qr_code_scanner</mat-icon>\n <span style=\"font-weight: 500; font-size: 9px;\">Connect Mobile</span>\n </button>\n </div>\n }\n </div>\n </div>\n </div>\n }\n }\n }\n </div>\n }\n @else if(!disabled){\n <!-- Main File Upload Area with Mobile Upload Button -->\n <div class=\"main-upload-wrapper\">\n <div class=\"main-upload-label\">\n <!-- Mobile Upload Section - Positioned at top with z-index -->\n @if(isRemoteUploadAvailable()){\n <div class=\"mobile-upload-overlay\" (click)=\"$event.stopPropagation()\">\n @if(isWaitingForMobileUpload) {\n <!-- Waiting for mobile upload state -->\n <div class=\"mobile-waiting-state\">\n <mat-progress-spinner diameter=\"24\" mode=\"indeterminate\"></mat-progress-spinner>\n <span style=\"font-size: 14px; color: #666; text-align: center;\">Waiting for upload from mobile...</span>\n <button type=\"button\" class=\"cancel-mobile-btn-text\" (click)=\"cancelMobileUpload(); $event.preventDefault(); $event.stopPropagation()\">\n Cancel\n </button>\n </div>\n } @else if(isRemotePaired()){\n <!-- Connected state - show upload and view connection buttons -->\n <div class=\"mobile-upload-actions\">\n <button type=\"button\" class=\"mobile-upload-btn connected\" (click)=\"triggerMobileUpload(); $event.preventDefault(); $event.stopPropagation()\" title=\"Upload from Mobile\">\n <mat-icon>smartphone</mat-icon>\n <span>Upload from Mobile</span>\n </button>\n <button type=\"button\" class=\"view-connection-btn\" (click)=\"openViewConnectionDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"View Connection\">\n <mat-icon>link</mat-icon>\n </button>\n </div>\n } @else {\n <!-- Not connected - show connect button -->\n <div class=\"mobile-upload-actions\">\n <button type=\"button\" class=\"mobile-upload-btn\" (click)=\"openRemoteUploadDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"Connect Mobile Device\">\n <mat-icon>qr_code_scanner</mat-icon>\n <span>Connect Mobile</span>\n </button>\n </div>\n }\n </div>\n }\n \n <!-- Upload Icon and Text - clickable area -->\n <label class=\"upload-content\" *ngIf=\"!isWaitingForMobileUpload\" [class.with-mobile]=\"isRemoteUploadAvailable()\" style=\"cursor: pointer; display: flex; flex-direction: column; gap: 12px; align-items: center; justify-content: center;\" (click)=\"loading ? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon>upload_file</mat-icon>\n <p style=\"text-align: center; font-size: 14px; margin: 0px;\">\n {{label}}\n @if(hint && hint != ''){\n <br>\n <small>{{hint}}</small>\n }\n </p>\n </label>\n </div>\n </div>\n }\n @else{\n <div class=\"not-found-section\" style=\"display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; cursor: pointer;\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" id=\"Layer_1\" x=\"0px\" y=\"0px\" width=\"100%\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M283.000000,513.000000 C188.689560,513.000000 94.879120,513.000000 1.034338,513.000000 C1.034338,342.397858 1.034338,171.795731 1.034338,1.096792 C171.560455,1.096792 342.121002,1.096792 512.840759,1.096792 C512.840759,171.666550 512.840759,342.333252 512.840759,513.000000 C436.462372,513.000000 359.981171,513.000000 283.000000,513.000000 M214.500000,497.000000 C271.998901,496.999969 329.497833,497.019897 386.996674,496.975769 C394.561920,496.969971 401.787231,494.626648 406.395233,488.770782 C412.026520,481.614502 416.644897,473.577057 421.072449,465.572754 C437.390259,436.072754 453.464813,406.438049 469.597321,376.835754 C478.327332,360.816742 487.426758,344.977722 495.530029,328.645905 C501.012421,317.596252 494.173737,304.613556 481.201294,304.861847 C456.377502,305.336975 431.538177,304.999878 406.705292,304.999847 C404.938568,304.999847 403.171844,304.999847 401.000031,304.999847 C401.000031,302.348328 401.003784,300.379181 400.999481,298.410065 C400.935120,269.085632 400.853577,239.761169 400.867645,210.436768 C400.868164,209.340317 401.642242,208.138214 402.315186,207.164581 C408.841400,197.722305 416.441071,188.858276 421.779449,178.792419 C433.290894,157.086823 435.985443,133.380402 431.740631,109.507538 C425.335449,73.484550 405.456726,46.387234 373.051300,29.163473 C356.219269,20.217096 338.040710,16.499708 318.773804,16.813351 C301.234619,17.098873 284.897461,21.133270 269.449402,28.965902 C256.098724,35.735111 244.580948,44.876842 234.887085,56.467457 C224.793365,68.536171 217.675415,82.055397 213.023590,96.957832 C209.186737,109.249481 208.654160,121.911049 208.964157,134.687836 C209.387650,152.143631 214.172058,168.302002 222.741714,183.427231 C224.412766,186.376572 225.987946,189.380234 227.939270,192.969376 C211.045273,192.969376 194.919281,193.199417 178.811310,192.763687 C175.805389,192.682358 172.083817,190.621887 170.038193,188.292847 C160.046188,176.916489 150.561493,165.096268 140.789429,153.524185 C136.493210,148.436600 131.117233,145.037399 124.221672,145.023544 C96.055626,144.966995 67.881424,145.387711 39.727192,144.808304 C28.532793,144.577896 16.775660,156.479080 16.809900,167.857864 C17.130381,274.354980 17.052288,380.853516 16.870073,487.351288 C16.859135,493.743988 19.582472,497.123566 26.503685,497.103851 C88.835381,496.926239 151.167801,497.000000 214.500000,497.000000 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M214.000000,497.000000 C151.167801,497.000000 88.835381,496.926239 26.503685,497.103851 C19.582472,497.123566 16.859135,493.743988 16.870073,487.351288 C17.052288,380.853516 17.130381,274.354980 16.809900,167.857864 C16.775660,156.479080 28.532793,144.577896 39.727192,144.808304 C67.881424,145.387711 96.055626,144.966995 124.221672,145.023544 C131.117233,145.037399 136.493210,148.436600 140.789429,153.524185 C150.561493,165.096268 160.046188,176.916489 170.038193,188.292847 C172.083817,190.621887 175.805389,192.682358 178.811310,192.763687 C194.919281,193.199417 211.045273,192.969376 227.939270,192.969376 C225.987946,189.380234 224.412766,186.376572 222.741714,183.427231 C214.172058,168.302002 209.387650,152.143631 208.964157,134.687836 C208.654160,121.911049 209.186737,109.249481 213.023590,96.957832 C217.675415,82.055397 224.793365,68.536171 234.887085,56.467457 C244.580948,44.876842 256.098724,35.735111 269.449402,28.965902 C284.897461,21.133270 301.234619,17.098873 318.773804,16.813351 C338.040710,16.499708 356.219269,20.217096 373.051300,29.163473 C405.456726,46.387234 425.335449,73.484550 431.740631,109.507538 C435.985443,133.380402 433.290894,157.086823 421.779449,178.792419 C416.441071,188.858276 408.841400,197.722305 402.315186,207.164581 C401.642242,208.138214 400.868164,209.340317 400.867645,210.436768 C400.853577,239.761169 400.935120,269.085632 400.999481,298.410065 C401.003784,300.379181 401.000031,302.348328 401.000031,304.999847 C403.171844,304.999847 404.938568,304.999847 406.705292,304.999847 C431.538177,304.999878 456.377502,305.336975 481.201294,304.861847 C494.173737,304.613556 501.012421,317.596252 495.530029,328.645905 C487.426758,344.977722 478.327332,360.816742 469.597321,376.835754 C453.464813,406.438049 437.390259,436.072754 421.072449,465.572754 C416.644897,473.577057 412.026520,481.614502 406.395233,488.770782 C401.787231,494.626648 394.561920,496.969971 386.996674,496.975769 C329.497833,497.019897 271.998901,496.999969 214.000000,497.000000 M134.597260,321.000000 C127.620270,319.337616 123.942146,323.017517 121.500618,328.932983 C120.498009,331.362091 119.193306,333.673279 117.937233,335.989990 C103.522736,362.575897 89.117485,389.166901 74.653091,415.725647 C67.257729,429.304626 59.689438,442.789490 52.312668,456.378448 C47.974022,464.370728 43.857620,472.483643 39.401810,481.000000 C42.117859,481.000000 44.110912,481.000000 46.103966,481.000000 C144.765793,481.000000 243.427612,481.000031 342.089447,481.000000 C354.922150,481.000000 367.755981,481.100647 380.587189,480.961823 C388.353729,480.877808 394.963684,478.668121 399.028687,471.088623 C414.189636,442.819641 429.540070,414.652191 444.852783,386.464783 C452.465210,372.451935 460.203308,358.507233 467.780884,344.475739 C471.870789,336.902466 475.742279,329.211273 480.010315,321.000000 C364.545990,321.000000 250.059738,321.000000 134.597260,321.000000 M324.557129,242.085190 C327.301666,237.590393 329.664886,232.791306 332.924042,228.707169 C335.118561,225.957092 338.299622,222.950409 341.520416,222.240189 C357.159485,218.791580 371.110535,211.909912 382.881226,201.408112 C405.027954,181.648727 416.679504,157.025070 416.129883,126.765366 C415.719910,104.193253 408.285522,84.336838 393.808441,67.607040 C373.699219,44.368736 348.121246,32.686760 316.779572,33.851212 C294.904785,34.663937 275.751617,42.202290 259.624786,56.188839 C232.720886,79.522171 221.513794,109.436241 227.057266,144.851562 C232.965317,182.596176 262.283539,213.393021 299.806335,221.925400 C304.366974,222.962448 307.440796,225.236969 309.702332,229.227615 C313.147400,235.306534 316.865295,241.230835 320.988098,248.086838 C322.400177,245.685776 323.276184,244.196213 324.557129,242.085190 z\"/>\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M135.085373,321.000000 C250.059738,321.000000 364.545990,321.000000 480.010315,321.000000 C475.742279,329.211273 471.870789,336.902466 467.780884,344.475739 C460.203308,358.507233 452.465210,372.451935 444.852783,386.464783 C429.540070,414.652191 414.189636,442.819641 399.028687,471.088623 C394.963684,478.668121 388.353729,480.877808 380.587189,480.961823 C367.755981,481.100647 354.922150,481.000000 342.089447,481.000000 C243.427612,481.000031 144.765793,481.000000 46.103966,481.000000 C44.110912,481.000000 42.117859,481.000000 39.401810,481.000000 C43.857620,472.483643 47.974022,464.370728 52.312668,456.378448 C59.689438,442.789490 67.257729,429.304626 74.653091,415.725647 C89.117485,389.166901 103.522736,362.575897 117.937233,335.989990 C119.193306,333.673279 120.498009,331.362091 121.500618,328.932983 C123.942146,323.017517 127.620270,319.337616 135.085373,321.000000 M140.500000,440.999939 C132.167938,440.999939 123.835869,440.999023 115.503807,441.000183 C106.978073,441.001343 104.965355,442.559265 105.000999,449.124817 C105.035042,455.395599 107.121445,456.999329 115.262733,456.999634 C146.424759,457.000732 177.586792,457.000153 208.748825,457.000000 C224.913086,456.999939 241.077591,456.952484 257.241425,457.035980 C260.763824,457.054169 263.472839,456.015015 264.705200,452.580017 C267.060974,446.013672 263.486053,441.009705 256.482880,441.007080 C218.155258,440.992767 179.827621,441.000061 140.500000,440.999939 M211.498047,417.000153 C216.330338,416.999176 221.164948,417.091248 225.994263,416.971069 C230.865005,416.849884 232.921509,414.514984 232.998932,409.368042 C233.083664,403.733337 231.121094,401.059509 226.230942,401.046417 C196.071045,400.965607 165.910004,400.915100 135.752502,401.195435 C133.579193,401.215637 130.361252,403.471161 129.469467,405.497375 C126.665665,411.867889 130.514359,416.987915 137.514694,416.993042 C161.842728,417.010773 186.170807,417.000061 211.498047,417.000153 z\"/>\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M324.354675,242.395920 C323.276184,244.196213 322.400177,245.685776 320.988098,248.086838 C316.865295,241.230835 313.147400,235.306534 309.702332,229.227615 C307.440796,225.236969 304.366974,222.962448 299.806335,221.925400 C262.283539,213.393021 232.965317,182.596176 227.057266,144.851562 C221.513794,109.436241 232.720886,79.522171 259.624786,56.188839 C275.751617,42.202290 294.904785,34.663937 316.779572,33.851212 C348.121246,32.686760 373.699219,44.368736 393.808441,67.607040 C408.285522,84.336838 415.719910,104.193253 416.129883,126.765366 C416.679504,157.025070 405.027954,181.648727 382.881226,201.408112 C371.110535,211.909912 357.159485,218.791580 341.520416,222.240189 C338.299622,222.950409 335.118561,225.957092 332.924042,228.707169 C329.664886,232.791306 327.301666,237.590393 324.354675,242.395920 M365.048187,97.443359 C370.583435,91.700768 370.701630,85.175507 365.330109,81.880753 C361.672668,79.637360 356.630035,80.933273 352.129089,85.405678 C342.914734,94.561584 333.749512,103.766907 324.548431,112.936203 C323.412384,114.068321 322.181274,115.104980 320.457794,116.674324 C319.017365,114.966263 317.930176,113.478592 316.646271,112.186401 C307.370972,102.851303 298.080811,93.530586 288.741608,84.259491 C284.745544,80.292557 279.208740,79.658867 275.876648,82.619972 C271.622955,86.400055 271.606628,92.007065 276.075226,96.532738 C285.555145,106.133804 295.140411,115.630798 304.670227,125.182716 C305.910522,126.425888 307.078339,127.741364 308.699982,129.471832 C306.767456,131.142502 305.170319,132.365768 303.755219,133.772186 C294.658264,142.813187 285.530975,151.825531 276.561829,160.992294 C272.316803,165.330841 271.679443,172.193146 274.860229,175.243683 C278.125275,178.375015 284.989929,177.494553 289.133881,173.366959 C295.270966,167.254105 301.415710,161.148209 307.489624,154.972931 C311.886749,150.502441 316.174255,145.924133 320.608521,141.291687 C322.557465,143.156174 323.772125,144.275360 324.939575,145.441833 C334.366547,154.861099 343.762360,164.311676 353.216248,173.703827 C357.078430,177.540771 364.054932,178.250854 367.156860,175.284821 C369.494812,173.049301 370.182617,164.901749 365.897583,161.258850 C361.343750,157.387436 357.354156,152.853470 353.106140,148.620880 C346.649475,142.187607 340.182373,135.764771 333.450989,129.070267 C344.099365,118.419060 354.322937,108.192757 365.048187,97.443359 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M141.000000,440.999939 C179.827621,441.000061 218.155258,440.992767 256.482880,441.007080 C263.486053,441.009705 267.060974,446.013672 264.705200,452.580017 C263.472839,456.015015 260.763824,457.054169 257.241425,457.035980 C241.077591,456.952484 224.913086,456.999939 208.748825,457.000000 C177.586792,457.000153 146.424759,457.000732 115.262733,456.999634 C107.121445,456.999329 105.035042,455.395599 105.000999,449.124817 C104.965355,442.559265 106.978073,441.001343 115.503807,441.000183 C123.835869,440.999023 132.167938,440.999939 141.000000,440.999939 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M210.998444,417.000153 C186.170807,417.000061 161.842728,417.010773 137.514694,416.993042 C130.514359,416.987915 126.665665,411.867889 129.469467,405.497375 C130.361252,403.471161 133.579193,401.215637 135.752502,401.195435 C165.910004,400.915100 196.071045,400.965607 226.230942,401.046417 C231.121094,401.059509 233.083664,403.733337 232.998932,409.368042 C232.921509,414.514984 230.865005,416.849884 225.994263,416.971069 C221.164948,417.091248 216.330338,416.999176 210.998444,417.000153 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M364.797363,97.704910 C354.322937,108.192757 344.099365,118.419060 333.450989,129.070267 C340.182373,135.764771 346.649475,142.187607 353.106140,148.620880 C357.354156,152.853470 361.343750,157.387436 365.897583,161.258850 C370.182617,164.901749 369.494812,173.049301 367.156860,175.284821 C364.054932,178.250854 357.078430,177.540771 353.216248,173.703827 C343.762360,164.311676 334.366547,154.861099 324.939575,145.441833 C323.772125,144.275360 322.557465,143.156174 320.608521,141.291687 C316.174255,145.924133 311.886749,150.502441 307.489624,154.972931 C301.415710,161.148209 295.270966,167.254105 289.133881,173.366959 C284.989929,177.494553 278.125275,178.375015 274.860229,175.243683 C271.679443,172.193146 272.316803,165.330841 276.561829,160.992294 C285.530975,151.825531 294.658264,142.813187 303.755219,133.772186 C305.170319,132.365768 306.767456,131.142502 308.699982,129.471832 C307.078339,127.741364 305.910522,126.425888 304.670227,125.182716 C295.140411,115.630798 285.555145,106.133804 276.075226,96.532738 C271.606628,92.007065 271.622955,86.400055 275.876648,82.619972 C279.208740,79.658867 284.745544,80.292557 288.741608,84.259491 C298.080811,93.530586 307.370972,102.851303 316.646271,112.186401 C317.930176,113.478592 319.017365,114.966263 320.457794,116.674324 C322.181274,115.104980 323.412384,114.068321 324.548431,112.936203 C333.749512,103.766907 342.914734,94.561584 352.129089,85.405678 C356.630035,80.933273 361.672668,79.637360 365.330109,81.880753 C370.701630,85.175507 370.583435,91.700768 364.797363,97.704910 z\"/>\n </svg>\n\n </div>\n }\n </div>\n }\n @else if(viewType == 'compact'){\n <div [class.image-picker-label-compact]=\"!previewOnly\" [class.error]=\"required && (filesArray?.length || 0) <= 0\" [class.upload-drag-over]=\"enableDragNDropForUpload && isUploadDragOver\" style=\"height: 42px; width: 42px;\" (dragover)=\"onUploadDragOver($event)\" (dragleave)=\"onUploadDragLeave($event)\" (drop)=\"onUploadDrop($event)\">\n @if(filesArray?.length){\n <div>\n @for(file of filesArray; track file.s3Url; let i = $index; let l = $last;){\n @if(i == 0) {\n <div>\n <div class=\"compact-image-item\" [class.selected]=\"config?.selectionMode ? selectedId == file.id : false\" [class.angular-curser-pointer]=\"!config?.hiddenPreview || config?.selectionMode\" id=\"image-item-{{config.selectorId}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i)\" [style.background-image]=\"type == 'file'? '' : 'url('+file.s3Url+')'\" (click)=\"onSelectFile(file)\" [style.background-size]=\"'contain'\">\n <mat-icon *ngIf=\"type == 'file'\" class=\"active angular-mat-icon\">description</mat-icon>\n <span class=\"material-icons cancel\" (click)=\"type == 'file'? deleteFile($event, {}, i, file) : deleteImage($event, {}, i, file)\" *ngIf=\"(!disabled || showDeleteButtonWhenDisabled) && !config.hiddenDeleteBtn && !file?.loading\">highlight_off</span>\n <span class=\"material-icons download\" (click)=\"$event.stopPropagation(); downloadFile(file.s3Url, file.title)\" *ngIf=\"!config.hiddenDownloadBtn && !file?.loading\">download_for_offline</span>\n \n <!-- Loading overlay -->\n <div class=\"loading-img\" *ngIf=\"file?.loading\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"30\"></mat-progress-spinner>\n </div>\n </div>\n \n </div>\n }\n @if(l && (config?.limit || 0) > (filesArray?.length || 0) && !disabled){\n <div class=\"flex flex-col\">\n <!-- Image upload options in grid -->\n <div class=\"image-item uploader\" id=\"image-item-{{config.selectorId}}-{{i}}-{{i}}\" style=\"display: flex; flex-direction: column; gap: 8px; justify-content: center; align-items: center; cursor: pointer;\" (click)=\"loading ? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon class=\"active\">upload_file</mat-icon>\n </div>\n </div>\n }\n }\n </div>\n }\n @else if(!disabled){\n <label style=\"display: flex; gap: 12px; flex-direction: column; justify-content: center; align-items: center; height: 100%; cursor: pointer;\" (click)=\"loading? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon>upload_file</mat-icon>\n </label>\n }\n @else{\n <div class=\"not-found-section\" style=\"display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; cursor: pointer;\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" id=\"Layer_1\" x=\"0px\" y=\"0px\" width=\"100%\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M283.000000,513.000000 C188.689560,513.000000 94.879120,513.000000 1.034338,513.000000 C1.034338,342.397858 1.034338,171.795731 1.034338,1.096792 C171.560455,1.096792 342.121002,1.096792 512.840759,1.096792 C512.840759,171.666550 512.840759,342.333252 512.840759,513.000000 C436.462372,513.000000 359.981171,513.000000 283.000000,513.000000 M214.500000,497.000000 C271.998901,496.999969 329.497833,497.019897 386.996674,496.975769 C394.561920,496.969971 401.787231,494.626648 406.395233,488.770782 C412.026520,481.614502 416.644897,473.577057 421.072449,465.572754 C437.390259,436.072754 453.464813,406.438049 469.597321,376.835754 C478.327332,360.816742 487.426758,344.977722 495.530029,328.645905 C501.012421,317.596252 494.173737,304.613556 481.201294,304.861847 C456.377502,305.336975 431.538177,304.999878 406.705292,304.999847 C404.938568,304.999847 403.171844,304.999847 401.000031,304.999847 C401.000031,302.348328 401.003784,300.379181 400.999481,298.410065 C400.935120,269.085632 400.853577,239.761169 400.867645,210.436768 C400.868164,209.340317 401.642242,208.138214 402.315186,207.164581 C408.841400,197.722305 416.441071,188.858276 421.779449,178.792419 C433.290894,157.086823 435.985443,133.380402 431.740631,109.507538 C425.335449,73.484550 405.456726,46.387234 373.051300,29.163473 C356.219269,20.217096 338.040710,16.499708 318.773804,16.813351 C301.234619,17.098873 284.897461,21.133270 269.449402,28.965902 C256.098724,35.735111 244.580948,44.876842 234.887085,56.467457 C224.793365,68.536171 217.675415,82.055397 213.023590,96.957832 C209.186737,109.249481 208.654160,121.911049 208.964157,134.687836 C209.387650,152.143631 214.172058,168.302002 222.741714,183.427231 C224.412766,186.376572 225.987946,189.380234 227.939270,192.969376 C211.045273,192.969376 194.919281,193.199417 178.811310,192.763687 C175.805389,192.682358 172.083817,190.621887 170.038193,188.292847 C160.046188,176.916489 150.561493,165.096268 140.789429,153.524185 C136.493210,148.436600 131.117233,145.037399 124.221672,145.023544 C96.055626,144.966995 67.881424,145.387711 39.727192,144.808304 C28.532793,144.577896 16.775660,156.479080 16.809900,167.857864 C17.130381,274.354980 17.052288,380.853516 16.870073,487.351288 C16.859135,493.743988 19.582472,497.123566 26.503685,497.103851 C88.835381,496.926239 151.167801,497.000000 214.500000,497.000000 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M214.000000,497.000000 C151.167801,497.000000 88.835381,496.926239 26.503685,497.103851 C19.582472,497.123566 16.859135,493.743988 16.870073,487.351288 C17.052288,380.853516 17.130381,274.354980 16.809900,167.857864 C16.775660,156.479080 28.532793,144.577896 39.727192,144.808304 C67.881424,145.387711 96.055626,144.966995 124.221672,145.023544 C131.117233,145.037399 136.493210,148.436600 140.789429,153.524185 C150.561493,165.096268 160.046188,176.916489 170.038193,188.292847 C172.083817,190.621887 175.805389,192.682358 178.811310,192.763687 C194.919281,193.199417 211.045273,192.969376 227.939270,192.969376 C225.987946,189.380234 224.412766,186.376572 222.741714,183.427231 C214.172058,168.302002 209.387650,152.143631 208.964157,134.687836 C208.654160,121.911049 209.186737,109.249481 213.023590,96.957832 C217.675415,82.055397 224.793365,68.536171 234.887085,56.467457 C244.580948,44.876842 256.098724,35.735111 269.449402,28.965902 C284.897461,21.133270 301.234619,17.098873 318.773804,16.813351 C338.040710,16.499708 356.219269,20.217096 373.051300,29.163473 C405.456726,46.387234 425.335449,73.484550 431.740631,109.507538 C435.985443,133.380402 433.290894,157.086823 421.779449,178.792419 C416.441071,188.858276 408.841400,197.722305 402.315186,207.164581 C401.642242,208.138214 400.868164,209.340317 400.867645,210.436768 C400.853577,239.761169 400.935120,269.085632 400.999481,298.410065 C401.003784,300.379181 401.000031,302.348328 401.000031,304.999847 C403.171844,304.999847 404.938568,304.999847 406.705292,304.999847 C431.538177,304.999878 456.377502,305.336975 481.201294,304.861847 C494.173737,304.613556 501.012421,317.596252 495.530029,328.645905 C487.426758,344.977722 478.327332,360.816742 469.597321,376.835754 C453.464813,406.438049 437.390259,436.072754 421.072449,465.572754 C416.644897,473.577057 412.026520,481.614502 406.395233,488.770782 C401.787231,494.626648 394.561920,496.969971 386.996674,496.975769 C329.497833,497.019897 271.998901,496.999969 214.000000,497.000000 M134.597260,321.000000 C127.620270,319.337616 123.942146,323.017517 121.500618,328.932983 C120.498009,331.362091 119.193306,333.673279 117.937233,335.989990 C103.522736,362.575897 89.117485,389.166901 74.653091,415.725647 C67.257729,429.304626 59.689438,442.789490 52.312668,456.378448 C47.974022,464.370728 43.857620,472.483643 39.401810,481.000000 C42.117859,481.000000 44.110912,481.000000 46.103966,481.000000 C144.765793,481.000000 243.427612,481.000031 342.089447,481.000000 C354.922150,481.000000 367.755981,481.100647 380.587189,480.961823 C388.353729,480.877808 394.963684,478.668121 399.028687,471.088623 C414.189636,442.819641 429.540070,414.652191 444.852783,386.464783 C452.465210,372.451935 460.203308,358.507233 467.780884,344.475739 C471.870789,336.902466 475.742279,329.211273 480.010315,321.000000 C364.545990,321.000000 250.059738,321.000000 134.597260,321.000000 M324.557129,242.085190 C327.301666,237.590393 329.664886,232.791306 332.924042,228.707169 C335.118561,225.957092 338.299622,222.950409 341.520416,222.240189 C357.159485,218.791580 371.110535,211.909912 382.881226,201.408112 C405.027954,181.648727 416.679504,157.025070 416.129883,126.765366 C415.719910,104.193253 408.285522,84.336838 393.808441,67.607040 C373.699219,44.368736 348.121246,32.686760 316.779572,33.851212 C294.904785,34.663937 275.751617,42.202290 259.624786,56.188839 C232.720886,79.522171 221.513794,109.436241 227.057266,144.851562 C232.965317,182.596176 262.283539,213.393021 299.806335,221.925400 C304.366974,222.962448 307.440796,225.236969 309.702332,229.227615 C313.147400,235.306534 316.865295,241.230835 320.988098,248.086838 C322.400177,245.685776 323.276184,244.196213 324.557129,242.085190 z\"/>\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M135.085373,321.000000 C250.059738,321.000000 364.545990,321.000000 480.010315,321.000000 C475.742279,329.211273 471.870789,336.902466 467.780884,344.475739 C460.203308,358.507233 452.465210,372.451935 444.852783,386.464783 C429.540070,414.652191 414.189636,442.819641 399.028687,471.088623 C394.963684,478.668121 388.353729,480.877808 380.587189,480.961823 C367.755981,481.100647 354.922150,481.000000 342.089447,481.000000 C243.427612,481.000031 144.765793,481.000000 46.103966,481.000000 C44.110912,481.000000 42.117859,481.000000 39.401810,481.000000 C43.857620,472.483643 47.974022,464.370728 52.312668,456.378448 C59.689438,442.789490 67.257729,429.304626 74.653091,415.725647 C89.117485,389.166901 103.522736,362.575897 117.937233,335.989990 C119.193306,333.673279 120.498009,331.362091 121.500618,328.932983 C123.942146,323.017517 127.620270,319.337616 135.085373,321.000000 M140.500000,440.999939 C132.167938,440.999939 123.835869,440.999023 115.503807,441.000183 C106.978073,441.001343 104.965355,442.559265 105.000999,449.124817 C105.035042,455.395599 107.121445,456.999329 115.262733,456.999634 C146.424759,457.000732 177.586792,457.000153 208.748825,457.000000 C224.913086,456.999939 241.077591,456.952484 257.241425,457.035980 C260.763824,457.054169 263.472839,456.015015 264.705200,452.580017 C267.060974,446.013672 263.486053,441.009705 256.482880,441.007080 C218.155258,440.992767 179.827621,441.000061 140.500000,440.999939 M211.498047,417.000153 C216.330338,416.999176 221.164948,417.091248 225.994263,416.971069 C230.865005,416.849884 232.921509,414.514984 232.998932,409.368042 C233.083664,403.733337 231.121094,401.059509 226.230942,401.046417 C196.071045,400.965607 165.910004,400.915100 135.752502,401.195435 C133.579193,401.215637 130.361252,403.471161 129.469467,405.497375 C126.665665,411.867889 130.514359,416.987915 137.514694,416.993042 C161.842728,417.010773 186.170807,417.000061 211.498047,417.000153 z\"/>\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M324.354675,242.395920 C323.276184,244.196213 322.400177,245.685776 320.988098,248.086838 C316.865295,241.230835 313.147400,235.306534 309.702332,229.227615 C307.440796,225.236969 304.366974,222.962448 299.806335,221.925400 C262.283539,213.393021 232.965317,182.596176 227.057266,144.851562 C221.513794,109.436241 232.720886,79.522171 259.624786,56.188839 C275.751617,42.202290 294.904785,34.663937 316.779572,33.851212 C348.121246,32.686760 373.699219,44.368736 393.808441,67.607040 C408.285522,84.336838 415.719910,104.193253 416.129883,126.765366 C416.679504,157.025070 405.027954,181.648727 382.881226,201.408112 C371.110535,211.909912 357.159485,218.791580 341.520416,222.240189 C338.299622,222.950409 335.118561,225.957092 332.924042,228.707169 C329.664886,232.791306 327.301666,237.590393 324.354675,242.395920 M365.048187,97.443359 C370.583435,91.700768 370.701630,85.175507 365.330109,81.880753 C361.672668,79.637360 356.630035,80.933273 352.129089,85.405678 C342.914734,94.561584 333.749512,103.766907 324.548431,112.936203 C323.412384,114.068321 322.181274,115.104980 320.457794,116.674324 C319.017365,114.966263 317.930176,113.478592 316.646271,112.186401 C307.370972,102.851303 298.080811,93.530586 288.741608,84.259491 C284.745544,80.292557 279.208740,79.658867 275.876648,82.619972 C271.622955,86.400055 271.606628,92.007065 276.075226,96.532738 C285.555145,106.133804 295.140411,115.630798 304.670227,125.182716 C305.910522,126.425888 307.078339,127.741364 308.699982,129.471832 C306.767456,131.142502 305.170319,132.365768 303.755219,133.772186 C294.658264,142.813187 285.530975,151.825531 276.561829,160.992294 C272.316803,165.330841 271.679443,172.193146 274.860229,175.243683 C278.125275,178.375015 284.989929,177.494553 289.133881,173.366959 C295.270966,167.254105 301.415710,161.148209 307.489624,154.972931 C311.886749,150.502441 316.174255,145.924133 320.608521,141.291687 C322.557465,143.156174 323.772125,144.275360 324.939575,145.441833 C334.366547,154.861099 343.762360,164.311676 353.216248,173.703827 C357.078430,177.540771 364.054932,178.250854 367.156860,175.284821 C369.494812,173.049301 370.182617,164.901749 365.897583,161.258850 C361.343750,157.387436 357.354156,152.853470 353.106140,148.620880 C346.649475,142.187607 340.182373,135.764771 333.450989,129.070267 C344.099365,118.419060 354.322937,108.192757 365.048187,97.443359 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M141.000000,440.999939 C179.827621,441.000061 218.155258,440.992767 256.482880,441.007080 C263.486053,441.009705 267.060974,446.013672 264.705200,452.580017 C263.472839,456.015015 260.763824,457.054169 257.241425,457.035980 C241.077591,456.952484 224.913086,456.999939 208.748825,457.000000 C177.586792,457.000153 146.424759,457.000732 115.262733,456.999634 C107.121445,456.999329 105.035042,455.395599 105.000999,449.124817 C104.965355,442.559265 106.978073,441.001343 115.503807,441.000183 C123.835869,440.999023 132.167938,440.999939 141.000000,440.999939 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M210.998444,417.000153 C186.170807,417.000061 161.842728,417.010773 137.514694,416.993042 C130.514359,416.987915 126.665665,411.867889 129.469467,405.497375 C130.361252,403.471161 133.579193,401.215637 135.752502,401.195435 C165.910004,400.915100 196.071045,400.965607 226.230942,401.046417 C231.121094,401.059509 233.083664,403.733337 232.998932,409.368042 C232.921509,414.514984 230.865005,416.849884 225.994263,416.971069 C221.164948,417.091248 216.330338,416.999176 210.998444,417.000153 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M364.797363,97.704910 C354.322937,108.192757 344.099365,118.419060 333.450989,129.070267 C340.182373,135.764771 346.649475,142.187607 353.106140,148.620880 C357.354156,152.853470 361.343750,157.387436 365.897583,161.258850 C370.182617,164.901749 369.494812,173.049301 367.156860,175.284821 C364.054932,178.250854 357.078430,177.540771 353.216248,173.703827 C343.762360,164.311676 334.366547,154.861099 324.939575,145.441833 C323.772125,144.275360 322.557465,143.156174 320.608521,141.291687 C316.174255,145.924133 311.886749,150.502441 307.489624,154.972931 C301.415710,161.148209 295.270966,167.254105 289.133881,173.366959 C284.989929,177.494553 278.125275,178.375015 274.860229,175.243683 C271.679443,172.193146 272.316803,165.330841 276.561829,160.992294 C285.530975,151.825531 294.658264,142.813187 303.755219,133.772186 C305.170319,132.365768 306.767456,131.142502 308.699982,129.471832 C307.078339,127.741364 305.910522,126.425888 304.670227,125.182716 C295.140411,115.630798 285.555145,106.133804 276.075226,96.532738 C271.606628,92.007065 271.622955,86.400055 275.876648,82.619972 C279.208740,79.658867 284.745544,80.292557 288.741608,84.259491 C298.080811,93.530586 307.370972,102.851303 316.646271,112.186401 C317.930176,113.478592 319.017365,114.966263 320.457794,116.674324 C322.181274,115.104980 323.412384,114.068321 324.548431,112.936203 C333.749512,103.766907 342.914734,94.561584 352.129089,85.405678 C356.630035,80.933273 361.672668,79.637360 365.330109,81.880753 C370.701630,85.175507 370.583435,91.700768 364.797363,97.704910 z\"/>\n </svg>\n\n </div>\n }\n </div>\n }\n @else if(viewType == 'list'){\n <div [class.error-border]=\"required && (filesArray?.length || 0) <= 0\" [class.upload-drag-over]=\"enableDragNDropForUpload && isUploadDragOver\" [style.height]=\"filesArray?.length ? null : config?.height\" style=\"width: 100%; display: flex; gap: 12px; flex-direction: column; --angular-image-picker-height: {{config?.height ?? 0}}\" (dragover)=\"onUploadDragOver($event)\" (dragleave)=\"onUploadDragLeave($event)\" (drop)=\"onUploadDrop($event)\">\n @if(filesArray?.length){\n @for(file of filesArray; track file.s3Url; let i = $index; let l = $last;){\n <div style=\"position: relative; display: flex; align-items: center; gap: 10px; border-radius: 5px; border: 1px solid black; padding: 5px; height: 56px;\">\n <mat-icon style=\"width: 40px; min-width: 22px;\">description</mat-icon>\n <div style=\"white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;\">{{file?.title}}</div>\n <div style=\"display: flex; align-items: center; gap: 4px;\" *ngIf=\"!file?.loading\">\n <button type=\"button\" mat-icon-button aria-label=\"Download File\" class=\"angular-icon-btn-sm angular-text-download\" style=\"margin: 0px !important;\" (click)=\"$event.stopPropagation(); downloadFile(file.s3Url, file.title)\" *ngIf=\"!config.hiddenDownloadBtn\">\n <mat-icon>download_for_offline</mat-icon>\n </button>\n <button type=\"button\" mat-icon-button aria-label=\"View File\" class=\"angular-icon-btn-sm angular-text-view\" style=\"margin: 0px !important;\" (click)=\"openFile(file)\" *ngIf=\"!config.hiddenPreview\">\n <mat-icon>visibility</mat-icon>\n </button>\n <button type=\"button\" mat-icon-button aria-label=\"Remove File\" class=\"angular-icon-btn-sm angular-text-cancel\" style=\"margin: 0px !important;\" (click)=\"type == 'file'? deleteFile($event, {}, i, file) : deleteImage($event, {}, i, file)\" *ngIf=\"(!disabled || showDeleteButtonWhenDisabled) && !config.hiddenDeleteBtn\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n \n <!-- Loading overlay for list view -->\n <div class=\"loading-img\" *ngIf=\"file?.loading\" style=\"border-radius: 5px;\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"25\"></mat-progress-spinner>\n </div>\n </div>\n @if(l && (config?.limit || 0) > (filesArray?.length || 0) && !disabled){\n <div style=\"cursor: pointer; display: flex; align-items: center; gap: 4px; border: 1px solid black; border-radius: 5px; padding: 5px; height: 56px;\" (click)=\"loading? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon style=\"width: 40px; min-width: 22px;\">upload_file</mat-icon>\n <div style=\"width: 100%;\">\n <div style=\"display: flex; flex-direction: column; align-items: start; font-size: 14px;\">\n {{label}}\n @if(hint && hint != ''){\n <small>{{hint}}</small>\n }\n </div>\n </div>\n </div>\n }\n }\n }\n @else if(!disabled){\n <div style=\"cursor: pointer; display: flex; align-items: center; gap: 4px; border: 1px solid black; border-radius: 5px; padding: 5px; height: 56px;\" (click)=\"loading? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon style=\"width: 40px; min-width: 22px;\">upload_file</mat-icon>\n <div style=\"width: 100%;\">\n <div style=\"display: flex; flex-direction: column; align-items: start; font-size: 14px;\">\n {{label}}\n @if(hint && hint != ''){\n <small>{{hint}}</small>\n }\n </div>\n </div>\n <!-- Mobile Upload Button for list view -->\n @if(isRemoteUploadAvailable()){\n <button type=\"button\" mat-icon-button class=\"mobile-upload-btn-list\" (click)=\"$event.stopPropagation(); openRemoteUploadDialog()\" title=\"Upload from Mobile\" aria-label=\"Upload from Mobile\">\n <mat-icon>smartphone</mat-icon>\n </button>\n }\n </div>\n }\n </div>\n }\n}\n", styles: [".hover-section{--overlay-display: none;position:relative}.hover-section .hover-section-overlay{display:var(--overlay-display)}.hover-section:hover{--overlay-display: flex !important}.w-full{width:100%!important}.h-full{height:100%!important}.cdk-drop-list-dragging{border:2px solid red!important}.cdk-drag-preview{box-sizing:border-box;border-radius:8px;box-shadow:0 5px 25px -5px #0000004d;opacity:.9}.cdk-drag-placeholder{opacity:.3}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drag-over-highlight .image-item{outline:3px solid #3f51b5!important;outline-offset:2px;box-shadow:0 0 15px #3f51b580!important}.grid{display:grid}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.angular-mat-icon-5x{width:125px!important;height:125px!important;font-size:125px!important}.angular-mat-icon-4x{width:100px!important;height:100px!important;font-size:100px!important}.angular-mat-icon-3x{width:75px!important;height:75px!important;font-size:75px!important}.angular-mat-icon-2x{width:50px!important;height:50px!important;font-size:50px!important}.angular-mat-icon{width:25px!important;height:25px!important;font-size:25px!important;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.angular-icon-btn-sm{margin-top:7px!important;padding:5px!important;height:36px!important;width:36px!important}.angular-text-download{color:var(--angular-download, var(--mat-sys-primary))}.angular-text-view{color:var(--angular-primary, var(--mat-sys-primary))}.angular-text-cancel{color:var(--angular-cancel, #bb333b)}::ng-deep #upload-img-box{padding:0;border:2px dashed #717171;display:grid;color:#717171;justify-content:center;align-items:center;width:100%;height:160px;background:#eaeaea;cursor:pointer}::ng-deep #upload-img-box input[type=file]{display:none}.preview-img{display:flex;justify-content:center;position:absolute;align-items:center;top:0;width:100%;height:100%;background:#9e9e9e59;opacity:0}.preview-img:hover{opacity:1}.img-box{order:2px solid #b5b5b5!important;position:relative;padding:5px;display:flex;justify-content:center;align-content:center;align-items:center}.loading-img{display:flex;justify-content:center;position:absolute;align-items:center;top:0;left:0;width:100%;height:100%;background:#9e9e9e59}.image-picker-label{border:none;border-radius:4px;padding:0;box-sizing:border-box;height:var(--angular-image-picker-height, 130px)}.image-picker-label:has(.image-list){border:2px solid #e3e8ef;padding:16px;height:auto}.image-picker-label.error{border:2px dashed var(--angular-error, #a00404);padding:16px}.image-picker-label-compact{border:1px dashed rgba(0,0,0,.38);border-radius:4px;padding:4px;box-sizing:border-box}.image-picker-label-compact.error{border:2px dashed var(--angular-error, #a00404)}.compact-image-item{width:42px;height:42px;position:relative;display:flex;align-items:center;justify-content:center}.compact-image-item .cancel{color:var(--angular-cancel, #bb333b);background-color:#fff;border-radius:20px;position:absolute;top:-10px;right:-10px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.compact-image-item .download{color:var(--angular-download, var(--mat-sys-primary));background-color:#fff;border-radius:20px;position:absolute;top:-10px;left:-10px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.image-list .image-item{aspect-ratio:1;height:var(--angular-image-picker-height, 130px);border-radius:5px;background-position:center;background-size:cover;position:relative;display:flex;justify-content:center;align-items:center;border:1px solid rgba(0,0,0,.38);width:100%}.image-list .image-item.uploader{border:2px dashed #d0d5dd!important;cursor:pointer;transition:all .3s ease;background:#fff}.image-list .image-item.uploader:hover{border-color:var(--angular-primary, #1F6AAD)!important;background:linear-gradient(to bottom,#fff,#f8f9fc);box-shadow:0 2px 8px #1f6aad14}.image-list .image-item:hover .mat-icon{display:unset!important}.image-list .image-item.selected{border:3px solid var(--angular-item-selected, var(--mat-sys-primary))!important;width:calc(100% - 4px)}.image-list .image-item .mat-icon{display:none}.image-list .image-item .mat-icon.active{display:unset!important}.image-list .image-item .cancel{color:var(--angular-cancel, #bb333b);background-color:#fff;border-radius:20px;position:absolute;top:-10px;right:-10px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.image-list .image-item .download{color:var(--angular-download, var(--mat-sys-primary));background-color:#fff;border-radius:20px;position:absolute;top:-10px;left:-10px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.image-list .image-item .drag{color:var(--angular-primary, var(--mat-sys-primary));background-color:#fff;border-radius:20px;position:absolute;top:-10px;left:30px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.image-list .image-item .shedded{background-color:#9e9e9ecc;border-radius:5px}.error-border{border:1px solid var(--angular-error-color, #a00404)!important;border-radius:5px!important}.download{color:var(--angular-download, var(--mat-sys-primary));position:absolute;top:-10px;left:-10px;cursor:pointer}.angular-curser-pointer{cursor:pointer}::ng-deep .angular-confirmation-dialog mat-dialog-container .mat-mdc-dialog-title{padding:8px 16px;display:flex;gap:10px;justify-content:start;width:100%;align-items:center}::ng-deep .angular-confirmation-dialog mat-dialog-container .mat-mdc-dialog-title:before{content:unset}::ng-deep .angular-confirmation-dialog mat-dialog-container .mat-mdc-dialog-content{padding-top:15px;font-size:14px}::ng-deep .angular-confirmation-dialog mat-dialog-container .mat-mdc-dialog-actions{border-top:1px solid rgba(0,0,0,.12);justify-content:end}.not-found-section svg{margin:auto;height:calc(var(--angular-image-picker-height) - 45px);max-height:150px}.image-description{width:100%;margin-top:.5rem;position:relative}.image-description-text{padding:.5rem;background-color:#f9fafb;border:1px solid #e5e7eb;border-radius:.375rem;font-size:.875rem;color:#4b5563}.image-description-text .anchor{color:#00f;cursor:pointer}.image-description-text .anchor:hover{text-decoration:underline}.edit-description-btn{color:#4b5563;border-radius:20px;position:absolute;right:.5rem;top:.5rem;cursor:pointer}.image-description-edit{display:flex;flex-direction:column}.image-description-edit textarea,.image-description-edit input{font-size:.875rem;line-height:1.25rem;padding:.5rem;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));border-width:1px;border-radius:.375rem;resize:none}.description-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.description-action-btn{padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;font-weight:500;background-image:none;cursor:pointer;box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}.description-cancel-btn{background-color:#f3f4f6;color:var(--angular-cancel);border:1px solid var(--angular-cancel)}.description-save-btn{background-color:var(--angular-primary, var(--mat-sys-primary));color:#fff}@media (max-width: 575.98px){.image-picker-label{padding:15px}}@media (min-width: 576px) and (max-width: 767.98px){.image-picker-label{padding:15px}}.main-upload-wrapper{position:relative;width:100%;overflow:hidden}.mobile-upload-overlay{position:absolute;top:13px;left:50%;transform:translate(-50%);z-index:10;padding:12px;display:flex;justify-content:center;align-items:center;box-sizing:border-box;pointer-events:all;width:100%}.mobile-upload-overlay-grid{padding:0;display:flex;justify-content:center;align-items:center;box-sizing:border-box;pointer-events:all}.upload-content{display:flex;flex-direction:column;gap:12px;align-items:center;justify-content:center;pointer-events:all;width:100%}.upload-content.with-mobile{margin-top:70px}.mobile-upload-section{padding:16px;background:linear-gradient(145deg,#fff,#f8f9fc);border:1px solid #e3e8ef;border-radius:12px;margin-bottom:16px;display:flex;justify-content:center;align-items:center;box-shadow:0 1px 3px #0000000d;box-sizing:border-box}.mobile-upload-section.grid-mobile-section{margin-bottom:10px;padding:10px}.mobile-upload-btn{display:flex;align-items:center;gap:8px;padding:12px 20px;background:linear-gradient(135deg,var(--angular-primary, #1F6AAD) 0%,#1a5a94 100%);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 8px #1f6aad33;letter-spacing:.3px;box-sizing:border-box}.mobile-upload-btn.compact{padding:8px 14px;font-size:13px}.mobile-upload-btn:hover{background:linear-gradient(135deg,#175080,#134066);transform:translateY(-2px);box-shadow:0 4px 12px #1f6aad4d}.mobile-upload-btn:active{transform:translateY(0);box-shadow:0 2px 4px #1f6aad33}.mobile-upload-btn.connected{background:linear-gradient(135deg,#4caf50,#388e3c);box-shadow:0 2px 8px #4caf5033}.mobile-upload-btn.connected:hover{background:linear-gradient(135deg,#388e3c,#2e7d32);box-shadow:0 4px 12px #4caf504d}.mobile-upload-btn mat-icon{font-size:20px;width:20px;height:20px}.mobile-upload-btn span{white-space:nowrap;font-weight:600}.mobile-upload-actions{display:flex;align-items:center;gap:8px}.main-upload-label{background:#fff;border:2px dashed #d0d5dd;border-radius:12px;padding:24px;transition:all .3s ease;height:calc(var(--angular-image-picker-height, 130px) + var(--angular-image-upload-config-height, 0px));box-sizing:border-box;position:relative;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;overflow:hidden}.main-upload-label:hover{border-color:var(--angular-primary, #1F6AAD);background:linear-gradient(to bottom,#fff,#f8f9fc);box-shadow:0 2px 8px #1f6aad14}.view-connection-btn{display:flex;align-items:center;justify-content:center;padding:5px;background:#fff;color:var(--angular-primary, #1F6AAD);border:2px solid var(--angular-primary, #1F6AAD);border-radius:8px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 4px #1f6aad1a}.view-connection-btn.compact{padding:8px}.view-connection-btn:hover{background:var(--angular-primary, #1F6AAD);color:#fff;transform:translateY(-2px);box-shadow:0 4px 12px #1f6aad40}.view-connection-btn:active{transform:translateY(0);box-shadow:0 2px 4px #1f6aad26}.view-connection-btn mat-icon{font-size:20px;width:20px;height:20px}.disconnect-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background-color:#f44336;color:#fff;border:none;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.disconnect-btn:hover{background-color:#d32f2f}.disconnect-btn mat-icon{font-size:18px;width:18px;height:18px}.mobile-waiting-state{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px;width:100%}.mobile-waiting-state .waiting-text{font-size:12px;color:#666}.mobile-waiting-state .cancel-mobile-btn{position:absolute;top:4px;right:4px;display:flex;align-items:center;justify-content:center;width:24px;height:24px;background-color:transparent;color:#666;border:none;border-radius:50%;cursor:pointer;transition:background-color .2s ease}.mobile-waiting-state .cancel-mobile-btn:hover{background-color:#0000001a}.mobile-waiting-state .cancel-mobile-btn mat-icon{font-size:16px;width:16px;height:16px}.cancel-mobile-btn-text{margin-top:8px;padding:6px 16px;background-color:transparent;color:var(--angular-primary, #1F6AAD);border:1px solid var(--angular-primary, #1F6AAD);border-radius:4px;cursor:pointer;font-size:12px;transition:background-color .2s ease}.cancel-mobile-btn-text:hover{background-color:#1f6aad1a}.mobile-upload-btn-list{display:flex;align-items:center;justify-content:center;width:40px;height:40px;background-color:var(--angular-primary, #1F6AAD);color:#fff;border:none;border-radius:50%;cursor:pointer;transition:background-color .2s ease;flex-shrink:0}.mobile-upload-btn-list:hover{background-color:var(--angular-primary-dark, #175080)}.mobile-upload-btn-list mat-icon{font-size:20px;width:20px;height:20px}.view-connection-btn{display:flex;align-items:center;justify-content:center;width:36px;height:36px;background-color:#e8f5e9;color:#4caf50;border:1px solid #4caf50;border-radius:4px;cursor:pointer;transition:all .2s ease}.view-connection-btn:hover{background-color:#c8e6c9}.view-connection-btn mat-icon{font-size:18px;width:18px;height:18px}.pending-files-container{display:flex;flex-direction:column;gap:8px;width:100%;max-height:200px;overflow-y:auto;padding:8px;background-color:#fff8e1;border-radius:4px;border:1px solid #ffcc80}.pending-files-header{display:flex;align-items:center;gap:6px;font-size:12px;color:#e65100;font-weight:500}.pending-files-header mat-icon{font-size:16px;width:16px;height:16px}.pending-files-list{display:flex;flex-direction:column;gap:6px}.pending-file-item{display:flex;align-items:center;gap:8px;padding:6px;background-color:#fff;border-radius:4px;border:1px solid #e0e0e0}.pending-file-preview{width:36px;height:36px;min-width:36px;border-radius:4px;background-size:cover;background-position:center;background-color:#f5f5f5;display:flex;align-items:center;justify-content:center}.pending-file-preview mat-icon{font-size:20px;color:#757575}.pending-file-name{flex:1;font-size:12px;color:#424242;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pending-file-actions{display:flex;gap:4px}.pending-file-actions .accept-btn{width:28px;height:28px;color:#4caf50}.pending-file-actions .accept-btn:hover{background-color:#4caf501a}.pending-file-actions .reject-btn{width:28px;height:28px;color:#f44336}.pending-file-actions .reject-btn:hover{background-color:#f443361a}.pending-file-actions mat-icon{font-size:18px;width:18px;height:18px}::ng-deep .camera-capture-dialog{padding:0!important;max-width:100vw!important;max-height:100vh!important}::ng-deep .camera-capture-dialog .mat-mdc-dialog-container{padding:0!important;background:transparent!important;box-shadow:none!important}.image-picker-label.upload-drag-over,.image-picker-label-compact.upload-drag-over,.error-border.upload-drag-over{outline:2px dashed var(--angular-primary, #1F6AAD);outline-offset:2px;background-color:#1f6aad0f}\n"], dependencies: [{ kind: "directive", type: i3$2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i3$2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3$2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i6.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i6.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i6.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: i3$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i4.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "component", type: i4$1.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "directive", type: i10.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i10.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i10.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }] });
3496
+ }
3497
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerComponent, decorators: [{
3498
+ type: Component,
3499
+ args: [{ selector: 'angular-image-and-file-picker', standalone: false, template: "@if (config) {\n <input type=\"file\" size=\"60\" id=\"{{config.selectorId}}\" (change)=\"type == 'file'? detectFiles($event): detectImages($event)\" accept=\"{{accept}}\" [multiple]=\"config.isMultiple\" style=\"display: none;\" />\n @if(viewType == 'card'){\n <div [class.image-picker-label]=\"!previewOnly\" [class.error]=\"required && (filesArray?.length || 0) <= 0\" [class.upload-drag-over]=\"enableDragNDropForUpload && isUploadDragOver\" [style.height]=\"filesArray?.length ? null : (remoteUploadConfig?.enabled ? ('calc(' + config?.height + ' + 40px)') : config?.height)\" style=\"--angular-image-picker-height: {{config?.height ?? 0}}; --angular-image-upload-config-height: {{remoteUploadConfig?.enabled ? '40px' : '0px'}}\" cdkDropList (cdkDropListDropped)=\"drop($event)\" cdkDropListOrientation=\"horizontal\" (dragover)=\"onUploadDragOver($event)\" (dragleave)=\"onUploadDragLeave($event)\" (drop)=\"onUploadDrop($event)\">\n @if(filesArray?.length){\n <div [ngClass]=\"{'grid-cols-1': config?.cols == 1, 'grid-cols-2': config?.cols == 2, 'grid-cols-3': config?.cols == 3, 'grid-cols-4': config?.cols == 4, 'grid-cols-5': config?.cols == 5, 'grid-cols-6': config?.cols == 6}\" class=\"{{previewInFlex ? 'flex' : 'grid'}} image-list\" style=\"gap: 25px;\">\n @for(file of filesArray; track file.s3Url; let i = $index; let l = $last;){\n @if(enableDragNDrop){\n <div class=\"flex flex-col drag-drop-item\" [attr.data-index]=\"i\" [class.drag-over-highlight]=\"dropTargetIndex === i && isDragging && dragSourceIndex !== i\">\n @if(isShowImageSequence){\n <div class=\"image-description\" style=\"margin-bottom: 15px;\">\n <div class=\"image-description-text\" style=\"text-align: center; font-weight: bold;\">Image {{i + 1}}</div>\n </div>\n }\n <div cdkDrag (cdkDragStarted)=\"onDragStarted(i)\" (cdkDragEnded)=\"onDragEnded()\" (cdkDragMoved)=\"onDragMoved($event)\">\n <div class=\"image-item {{imageItemClass}}\" [class.selected]=\"config?.selectionMode ? selectedId == file.id : false\" [class.angular-curser-pointer]=\"!config?.hiddenPreview || config?.selectionMode\" id=\"image-item-{{config.selectorId}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i)\" [style.background-image]=\"type == 'file'? '' : 'url('+file.s3Url+')'\" (click)=\"onSelectFile(file)\" cdkDragHandle>\n <mat-icon *ngIf=\"type == 'file'\" class=\"active angular-mat-icon-2x\">description</mat-icon>\n <span class=\"material-icons cancel\" (click)=\"type == 'file'? deleteFile($event, {}, i, file) : deleteImage($event, {}, i, file)\" *ngIf=\"(!disabled || showDeleteButtonWhenDisabled) && !config.hiddenDeleteBtn && !file?.loading\">highlight_off</span>\n <span class=\"material-icons download\" (click)=\"$event.stopPropagation(); downloadFile(file.s3Url, file.title)\" *ngIf=\"!config.hiddenDownloadBtn && !file?.loading\">download_for_offline</span>\n \n <!-- Loading overlay -->\n <div class=\"loading-img\" *ngIf=\"file?.loading\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"30\"></mat-progress-spinner>\n </div>\n </div>\n </div>\n @if(config?.enableImageTags){\n <div class=\"image-description\">\n <div class=\"image-description-text\" *ngIf=\"!file?.isEditMode\">\n @if(file?.tags && file?.tags != ''){\n <span class=\"anchor\" *ngFor=\"let tag of getTagsArray(file?.tags)\">{{tag}} </span>\n }\n @else {\n <span>No Tags</span>\n }\n </div>\n <span class=\"material-icons edit-description-btn edit\" style=\"font-size: 20px;\" (click)=\"$event.stopPropagation(); file.tempTags = file.tags; file.isEditMode = true;\" *ngIf=\"!file?.isEditMode && (editTagButtonWhenDisabled || !disabled)\">edit</span>\n <div class=\"image-description-edit\" *ngIf=\"file?.isEditMode\">\n <!-- (keydown)=\"onKeydown($event, file)\" -->\n <input [(ngModel)]=\"file.tempTags\" placeholder=\"Enter tags here...\" (keydown.enter)=\"onSubmitTags(file)\" (keydown.space)=\"editTagWithSpace(file)\" />\n <div class=\"description-actions\">\n <button class=\"description-action-btn description-cancel-btn\" (click)=\"$event.stopPropagation(); file.tempTags = file.tags; file.isEditMode = false;\">Cancel</button>\n <button class=\"description-action-btn description-save-btn\" (click)=\"$event.stopPropagation(); onSubmitTags(file);\">Save</button>\n </div>\n </div>\n </div>\n }\n </div>\n }\n @else {\n <div class=\"flex flex-col\">\n @if(isShowImageSequence){\n <div class=\"image-description\" style=\"margin-bottom: 15px;\">\n <div class=\"image-description-text\" style=\"text-align: center; font-weight: bold;\">Image {{i + 1}}</div>\n </div>\n }\n <div class=\"image-item {{imageItemClass}}\" [class.selected]=\"config?.selectionMode ? selectedId == file.id : false\" [class.angular-curser-pointer]=\"!config?.hiddenPreview || config?.selectionMode\" id=\"image-item-{{config.selectorId}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i)\" [style.background-image]=\"type == 'file'? '' : 'url('+file.s3Url+')'\" (click)=\"onSelectFile(file)\">\n <mat-icon *ngIf=\"type == 'file'\" class=\"active angular-mat-icon-2x\">description</mat-icon>\n <span class=\"material-icons cancel\" (click)=\"type == 'file'? deleteFile($event, {}, i, file) : deleteImage($event, {}, i, file)\" *ngIf=\"(!disabled || showDeleteButtonWhenDisabled) && !config.hiddenDeleteBtn && !file?.loading\">highlight_off</span>\n <span class=\"material-icons download\" (click)=\"$event.stopPropagation(); downloadFile(file.s3Url, file.title)\" *ngIf=\"!config.hiddenDownloadBtn && !file?.loading\">download_for_offline</span>\n \n <!-- Loading overlay -->\n <div class=\"loading-img\" *ngIf=\"file?.loading\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"30\"></mat-progress-spinner>\n </div>\n </div>\n @if(config?.enableImageTags){\n <div class=\"image-description\">\n <div class=\"image-description-text\" *ngIf=\"!file?.isEditMode\">{{file?.tags || 'No Tags'}}</div>\n <span class=\"material-icons edit-description-btn edit\" style=\"font-size: 20px;\" (click)=\"$event.stopPropagation(); file.isEditMode = true;\" *ngIf=\"!file?.isEditMode && !disabled\">edit</span>\n <div class=\"image-description-edit\" *ngIf=\"file?.isEditMode && !disabled\">\n <!-- (keydown)=\"onKeydown($event, file)\" -->\n <input [(ngModel)]=\"file.tempTags\" placeholder=\"Enter tags here...\" (keydown.enter)=\"onSubmitTags(file)\" (keydown.space)=\"editTagWithSpace(file)\" />\n <div class=\"description-actions\">\n <button class=\"description-action-btn description-cancel-btn\" (click)=\"$event.stopPropagation(); file.tempTags = file.tags; file.isEditMode = false;\">Cancel</button>\n <button class=\"description-action-btn description-save-btn\" (click)=\"$event.stopPropagation(); onSubmitTags(file);\">Save</button>\n </div>\n </div>\n </div>\n }\n </div>\n }\n @if(l && (config?.limit || 0) > (filesArray?.length || 0) && !disabled){\n <div class=\"flex flex-col\" style=\"position: relative;\">\n @if(isShowImageSequence){\n <div style=\"margin-top: .5rem; margin-bottom: 15px; height: 35px;\">\n </div>\n }\n \n <!-- Image upload options in grid -->\n <div class=\"image-item uploader\" id=\"image-item-{{config.selectorId}}-{{i}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i+'-'+i, -4)\" style=\"display: flex; flex-direction: column; gap: 8px; justify-content: center; align-items: center; cursor: pointer; position: relative;\" (click)=\"loading ? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n @if(isWaitingForMobileUpload) {\n <!-- Waiting for mobile upload state -->\n <div class=\"mobile-waiting-state hover-section\" style=\"gap: 4px; padding: 8px;\" (click)=\"$event.stopPropagation()\">\n <mat-progress-spinner diameter=\"18\" mode=\"indeterminate\"></mat-progress-spinner>\n <span class=\"waiting-text\" style=\"font-size: 10px; text-align: center;\">Waiting for mobile...</span>\n <div class=\"w-full h-full absolute hover-section-overlay\" style=\"top: 0; left: 0; background-color: #fafafa; justify-content: center; align-items: center;\">\n <button type=\"button\" class=\"cancel-mobile-btn\" style=\"position: unset;\" (click)=\"cancelMobileUpload(); $event.stopPropagation(); $event.preventDefault()\" title=\"Cancel\">\n <mat-icon style=\"display: unset; color: var(--angular-cancel, #bb333b);\">close</mat-icon>\n </button>\n </div>\n </div>\n } @else if(pendingFiles.length > 0) {\n <!-- Pending files from mobile - show accept/reject UI -->\n <div class=\"pending-files-container\" (click)=\"$event.stopPropagation()\">\n <div class=\"pending-files-header\">\n <mat-icon>smartphone</mat-icon>\n <span>{{ pendingFiles.length }} file(s) from mobile</span>\n </div>\n <div class=\"pending-files-list\">\n @for(pf of pendingFiles; track pf.timestamp; let idx = $index) {\n <div class=\"pending-file-item\">\n <div class=\"pending-file-preview\" [style.background-image]=\"isImageMimeType(pf.file.mimeType) ? 'url(' + pf.file.s3Url + ')' : ''\">\n @if(!isImageMimeType(pf.file.mimeType)) {\n <mat-icon>description</mat-icon>\n }\n </div>\n <span class=\"pending-file-name\" [title]=\"pf.file.fileName\">{{ pf.file.fileName }}</span>\n <div class=\"pending-file-actions\">\n <button type=\"button\" mat-icon-button class=\"accept-btn\" (click)=\"acceptPendingFile(pf); $event.stopPropagation()\" title=\"Accept\">\n <mat-icon>check</mat-icon>\n </button>\n <button type=\"button\" mat-icon-button class=\"reject-btn\" (click)=\"rejectPendingFile(pf); $event.stopPropagation()\" title=\"Reject\">\n <mat-icon>close</mat-icon>\n </button>\n </div>\n </div>\n }\n </div>\n </div>\n } @else{\n <div class=\"w-full h-full\" style=\"display: flex; flex-direction: column; gap: 8px; justify-content: center; align-items: center;\">\n <mat-icon class=\"active\">upload_file</mat-icon>\n <span style=\"font-weight: 500; font-size: 9px;\">{{label}}</span>\n </div>\n }\n </div>\n </div>\n\n @if(isRemoteUploadAvailable() && !isWaitingForMobileUpload && pendingFiles.length === 0){\n <div class=\"flex flex-col\" style=\"position: relative;\">\n @if(isShowImageSequence){\n <div style=\"margin-top: .5rem; margin-bottom: 15px; height: 35px;\">\n </div>\n }\n \n <div class=\"image-item uploader\" id=\"image-item-{{config.selectorId}}-{{i}}-{{i}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i)\" style=\"display: flex; flex-direction: column; gap: 8px; justify-content: center; align-items: center; cursor: pointer; position: relative; border-width: 0px !important;\">\n <!-- Mobile Upload Section - Positioned at top-right of upload item -->\n @if(isRemotePaired()){\n <span class=\"material-icons cancel\" style=\"color: #4caf50;\" (click)=\"openViewConnectionDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"View Connection\">link</span>\n }\n <div class=\"mobile-upload-overlay-grid w-full h-full\">\n @if(isRemotePaired()){\n <!-- Connected state -->\n <div class=\"mobile-upload-actions w-full h-full\">\n <button type=\"button\" class=\"mobile-upload-btn connected compact\" style=\"width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;\" (click)=\"triggerMobileUpload(); $event.preventDefault(); $event.stopPropagation()\" title=\"Upload from Mobile\">\n <mat-icon style=\"display: unset !important;\">mobile_screen_share</mat-icon>\n <span style=\"font-weight: 500; font-size: 9px;\">Mobile Upload</span>\n </button>\n <!-- <button type=\"button\" class=\"view-connection-btn compact\" (click)=\"openViewConnectionDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"View Connection\">\n <mat-icon style=\"display: unset !important;\">link</mat-icon>\n </button> -->\n </div>\n } @else {\n <!-- Not connected -->\n <div class=\"mobile-upload-actions w-full h-full\">\n <button type=\"button\" class=\"mobile-upload-btn compact\" style=\"width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;\" (click)=\"openRemoteUploadDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"Connect Mobile Device\">\n <mat-icon style=\"display: unset !important;\">qr_code_scanner</mat-icon>\n <span style=\"font-weight: 500; font-size: 9px;\">Connect Mobile</span>\n </button>\n </div>\n }\n </div>\n </div>\n </div>\n }\n }\n }\n </div>\n }\n @else if(!disabled){\n <!-- Main File Upload Area with Mobile Upload Button -->\n <div class=\"main-upload-wrapper\">\n <div class=\"main-upload-label\">\n <!-- Mobile Upload Section - Positioned at top with z-index -->\n @if(isRemoteUploadAvailable()){\n <div class=\"mobile-upload-overlay\" (click)=\"$event.stopPropagation()\">\n @if(isWaitingForMobileUpload) {\n <!-- Waiting for mobile upload state -->\n <div class=\"mobile-waiting-state\">\n <mat-progress-spinner diameter=\"24\" mode=\"indeterminate\"></mat-progress-spinner>\n <span style=\"font-size: 14px; color: #666; text-align: center;\">Waiting for upload from mobile...</span>\n <button type=\"button\" class=\"cancel-mobile-btn-text\" (click)=\"cancelMobileUpload(); $event.preventDefault(); $event.stopPropagation()\">\n Cancel\n </button>\n </div>\n } @else if(isRemotePaired()){\n <!-- Connected state - show upload and view connection buttons -->\n <div class=\"mobile-upload-actions\">\n <button type=\"button\" class=\"mobile-upload-btn connected\" (click)=\"triggerMobileUpload(); $event.preventDefault(); $event.stopPropagation()\" title=\"Upload from Mobile\">\n <mat-icon>smartphone</mat-icon>\n <span>Upload from Mobile</span>\n </button>\n <button type=\"button\" class=\"view-connection-btn\" (click)=\"openViewConnectionDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"View Connection\">\n <mat-icon>link</mat-icon>\n </button>\n </div>\n } @else {\n <!-- Not connected - show connect button -->\n <div class=\"mobile-upload-actions\">\n <button type=\"button\" class=\"mobile-upload-btn\" (click)=\"openRemoteUploadDialog(); $event.preventDefault(); $event.stopPropagation()\" title=\"Connect Mobile Device\">\n <mat-icon>qr_code_scanner</mat-icon>\n <span>Connect Mobile</span>\n </button>\n </div>\n }\n </div>\n }\n \n <!-- Upload Icon and Text - clickable area -->\n <label class=\"upload-content\" *ngIf=\"!isWaitingForMobileUpload\" [class.with-mobile]=\"isRemoteUploadAvailable()\" style=\"cursor: pointer; display: flex; flex-direction: column; gap: 12px; align-items: center; justify-content: center;\" (click)=\"loading ? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon>upload_file</mat-icon>\n <p style=\"text-align: center; font-size: 14px; margin: 0px;\">\n {{label}}\n @if(hint && hint != ''){\n <br>\n <small>{{hint}}</small>\n }\n </p>\n </label>\n </div>\n </div>\n }\n @else{\n <div class=\"not-found-section\" style=\"display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; cursor: pointer;\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" id=\"Layer_1\" x=\"0px\" y=\"0px\" width=\"100%\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M283.000000,513.000000 C188.689560,513.000000 94.879120,513.000000 1.034338,513.000000 C1.034338,342.397858 1.034338,171.795731 1.034338,1.096792 C171.560455,1.096792 342.121002,1.096792 512.840759,1.096792 C512.840759,171.666550 512.840759,342.333252 512.840759,513.000000 C436.462372,513.000000 359.981171,513.000000 283.000000,513.000000 M214.500000,497.000000 C271.998901,496.999969 329.497833,497.019897 386.996674,496.975769 C394.561920,496.969971 401.787231,494.626648 406.395233,488.770782 C412.026520,481.614502 416.644897,473.577057 421.072449,465.572754 C437.390259,436.072754 453.464813,406.438049 469.597321,376.835754 C478.327332,360.816742 487.426758,344.977722 495.530029,328.645905 C501.012421,317.596252 494.173737,304.613556 481.201294,304.861847 C456.377502,305.336975 431.538177,304.999878 406.705292,304.999847 C404.938568,304.999847 403.171844,304.999847 401.000031,304.999847 C401.000031,302.348328 401.003784,300.379181 400.999481,298.410065 C400.935120,269.085632 400.853577,239.761169 400.867645,210.436768 C400.868164,209.340317 401.642242,208.138214 402.315186,207.164581 C408.841400,197.722305 416.441071,188.858276 421.779449,178.792419 C433.290894,157.086823 435.985443,133.380402 431.740631,109.507538 C425.335449,73.484550 405.456726,46.387234 373.051300,29.163473 C356.219269,20.217096 338.040710,16.499708 318.773804,16.813351 C301.234619,17.098873 284.897461,21.133270 269.449402,28.965902 C256.098724,35.735111 244.580948,44.876842 234.887085,56.467457 C224.793365,68.536171 217.675415,82.055397 213.023590,96.957832 C209.186737,109.249481 208.654160,121.911049 208.964157,134.687836 C209.387650,152.143631 214.172058,168.302002 222.741714,183.427231 C224.412766,186.376572 225.987946,189.380234 227.939270,192.969376 C211.045273,192.969376 194.919281,193.199417 178.811310,192.763687 C175.805389,192.682358 172.083817,190.621887 170.038193,188.292847 C160.046188,176.916489 150.561493,165.096268 140.789429,153.524185 C136.493210,148.436600 131.117233,145.037399 124.221672,145.023544 C96.055626,144.966995 67.881424,145.387711 39.727192,144.808304 C28.532793,144.577896 16.775660,156.479080 16.809900,167.857864 C17.130381,274.354980 17.052288,380.853516 16.870073,487.351288 C16.859135,493.743988 19.582472,497.123566 26.503685,497.103851 C88.835381,496.926239 151.167801,497.000000 214.500000,497.000000 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M214.000000,497.000000 C151.167801,497.000000 88.835381,496.926239 26.503685,497.103851 C19.582472,497.123566 16.859135,493.743988 16.870073,487.351288 C17.052288,380.853516 17.130381,274.354980 16.809900,167.857864 C16.775660,156.479080 28.532793,144.577896 39.727192,144.808304 C67.881424,145.387711 96.055626,144.966995 124.221672,145.023544 C131.117233,145.037399 136.493210,148.436600 140.789429,153.524185 C150.561493,165.096268 160.046188,176.916489 170.038193,188.292847 C172.083817,190.621887 175.805389,192.682358 178.811310,192.763687 C194.919281,193.199417 211.045273,192.969376 227.939270,192.969376 C225.987946,189.380234 224.412766,186.376572 222.741714,183.427231 C214.172058,168.302002 209.387650,152.143631 208.964157,134.687836 C208.654160,121.911049 209.186737,109.249481 213.023590,96.957832 C217.675415,82.055397 224.793365,68.536171 234.887085,56.467457 C244.580948,44.876842 256.098724,35.735111 269.449402,28.965902 C284.897461,21.133270 301.234619,17.098873 318.773804,16.813351 C338.040710,16.499708 356.219269,20.217096 373.051300,29.163473 C405.456726,46.387234 425.335449,73.484550 431.740631,109.507538 C435.985443,133.380402 433.290894,157.086823 421.779449,178.792419 C416.441071,188.858276 408.841400,197.722305 402.315186,207.164581 C401.642242,208.138214 400.868164,209.340317 400.867645,210.436768 C400.853577,239.761169 400.935120,269.085632 400.999481,298.410065 C401.003784,300.379181 401.000031,302.348328 401.000031,304.999847 C403.171844,304.999847 404.938568,304.999847 406.705292,304.999847 C431.538177,304.999878 456.377502,305.336975 481.201294,304.861847 C494.173737,304.613556 501.012421,317.596252 495.530029,328.645905 C487.426758,344.977722 478.327332,360.816742 469.597321,376.835754 C453.464813,406.438049 437.390259,436.072754 421.072449,465.572754 C416.644897,473.577057 412.026520,481.614502 406.395233,488.770782 C401.787231,494.626648 394.561920,496.969971 386.996674,496.975769 C329.497833,497.019897 271.998901,496.999969 214.000000,497.000000 M134.597260,321.000000 C127.620270,319.337616 123.942146,323.017517 121.500618,328.932983 C120.498009,331.362091 119.193306,333.673279 117.937233,335.989990 C103.522736,362.575897 89.117485,389.166901 74.653091,415.725647 C67.257729,429.304626 59.689438,442.789490 52.312668,456.378448 C47.974022,464.370728 43.857620,472.483643 39.401810,481.000000 C42.117859,481.000000 44.110912,481.000000 46.103966,481.000000 C144.765793,481.000000 243.427612,481.000031 342.089447,481.000000 C354.922150,481.000000 367.755981,481.100647 380.587189,480.961823 C388.353729,480.877808 394.963684,478.668121 399.028687,471.088623 C414.189636,442.819641 429.540070,414.652191 444.852783,386.464783 C452.465210,372.451935 460.203308,358.507233 467.780884,344.475739 C471.870789,336.902466 475.742279,329.211273 480.010315,321.000000 C364.545990,321.000000 250.059738,321.000000 134.597260,321.000000 M324.557129,242.085190 C327.301666,237.590393 329.664886,232.791306 332.924042,228.707169 C335.118561,225.957092 338.299622,222.950409 341.520416,222.240189 C357.159485,218.791580 371.110535,211.909912 382.881226,201.408112 C405.027954,181.648727 416.679504,157.025070 416.129883,126.765366 C415.719910,104.193253 408.285522,84.336838 393.808441,67.607040 C373.699219,44.368736 348.121246,32.686760 316.779572,33.851212 C294.904785,34.663937 275.751617,42.202290 259.624786,56.188839 C232.720886,79.522171 221.513794,109.436241 227.057266,144.851562 C232.965317,182.596176 262.283539,213.393021 299.806335,221.925400 C304.366974,222.962448 307.440796,225.236969 309.702332,229.227615 C313.147400,235.306534 316.865295,241.230835 320.988098,248.086838 C322.400177,245.685776 323.276184,244.196213 324.557129,242.085190 z\"/>\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M135.085373,321.000000 C250.059738,321.000000 364.545990,321.000000 480.010315,321.000000 C475.742279,329.211273 471.870789,336.902466 467.780884,344.475739 C460.203308,358.507233 452.465210,372.451935 444.852783,386.464783 C429.540070,414.652191 414.189636,442.819641 399.028687,471.088623 C394.963684,478.668121 388.353729,480.877808 380.587189,480.961823 C367.755981,481.100647 354.922150,481.000000 342.089447,481.000000 C243.427612,481.000031 144.765793,481.000000 46.103966,481.000000 C44.110912,481.000000 42.117859,481.000000 39.401810,481.000000 C43.857620,472.483643 47.974022,464.370728 52.312668,456.378448 C59.689438,442.789490 67.257729,429.304626 74.653091,415.725647 C89.117485,389.166901 103.522736,362.575897 117.937233,335.989990 C119.193306,333.673279 120.498009,331.362091 121.500618,328.932983 C123.942146,323.017517 127.620270,319.337616 135.085373,321.000000 M140.500000,440.999939 C132.167938,440.999939 123.835869,440.999023 115.503807,441.000183 C106.978073,441.001343 104.965355,442.559265 105.000999,449.124817 C105.035042,455.395599 107.121445,456.999329 115.262733,456.999634 C146.424759,457.000732 177.586792,457.000153 208.748825,457.000000 C224.913086,456.999939 241.077591,456.952484 257.241425,457.035980 C260.763824,457.054169 263.472839,456.015015 264.705200,452.580017 C267.060974,446.013672 263.486053,441.009705 256.482880,441.007080 C218.155258,440.992767 179.827621,441.000061 140.500000,440.999939 M211.498047,417.000153 C216.330338,416.999176 221.164948,417.091248 225.994263,416.971069 C230.865005,416.849884 232.921509,414.514984 232.998932,409.368042 C233.083664,403.733337 231.121094,401.059509 226.230942,401.046417 C196.071045,400.965607 165.910004,400.915100 135.752502,401.195435 C133.579193,401.215637 130.361252,403.471161 129.469467,405.497375 C126.665665,411.867889 130.514359,416.987915 137.514694,416.993042 C161.842728,417.010773 186.170807,417.000061 211.498047,417.000153 z\"/>\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M324.354675,242.395920 C323.276184,244.196213 322.400177,245.685776 320.988098,248.086838 C316.865295,241.230835 313.147400,235.306534 309.702332,229.227615 C307.440796,225.236969 304.366974,222.962448 299.806335,221.925400 C262.283539,213.393021 232.965317,182.596176 227.057266,144.851562 C221.513794,109.436241 232.720886,79.522171 259.624786,56.188839 C275.751617,42.202290 294.904785,34.663937 316.779572,33.851212 C348.121246,32.686760 373.699219,44.368736 393.808441,67.607040 C408.285522,84.336838 415.719910,104.193253 416.129883,126.765366 C416.679504,157.025070 405.027954,181.648727 382.881226,201.408112 C371.110535,211.909912 357.159485,218.791580 341.520416,222.240189 C338.299622,222.950409 335.118561,225.957092 332.924042,228.707169 C329.664886,232.791306 327.301666,237.590393 324.354675,242.395920 M365.048187,97.443359 C370.583435,91.700768 370.701630,85.175507 365.330109,81.880753 C361.672668,79.637360 356.630035,80.933273 352.129089,85.405678 C342.914734,94.561584 333.749512,103.766907 324.548431,112.936203 C323.412384,114.068321 322.181274,115.104980 320.457794,116.674324 C319.017365,114.966263 317.930176,113.478592 316.646271,112.186401 C307.370972,102.851303 298.080811,93.530586 288.741608,84.259491 C284.745544,80.292557 279.208740,79.658867 275.876648,82.619972 C271.622955,86.400055 271.606628,92.007065 276.075226,96.532738 C285.555145,106.133804 295.140411,115.630798 304.670227,125.182716 C305.910522,126.425888 307.078339,127.741364 308.699982,129.471832 C306.767456,131.142502 305.170319,132.365768 303.755219,133.772186 C294.658264,142.813187 285.530975,151.825531 276.561829,160.992294 C272.316803,165.330841 271.679443,172.193146 274.860229,175.243683 C278.125275,178.375015 284.989929,177.494553 289.133881,173.366959 C295.270966,167.254105 301.415710,161.148209 307.489624,154.972931 C311.886749,150.502441 316.174255,145.924133 320.608521,141.291687 C322.557465,143.156174 323.772125,144.275360 324.939575,145.441833 C334.366547,154.861099 343.762360,164.311676 353.216248,173.703827 C357.078430,177.540771 364.054932,178.250854 367.156860,175.284821 C369.494812,173.049301 370.182617,164.901749 365.897583,161.258850 C361.343750,157.387436 357.354156,152.853470 353.106140,148.620880 C346.649475,142.187607 340.182373,135.764771 333.450989,129.070267 C344.099365,118.419060 354.322937,108.192757 365.048187,97.443359 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M141.000000,440.999939 C179.827621,441.000061 218.155258,440.992767 256.482880,441.007080 C263.486053,441.009705 267.060974,446.013672 264.705200,452.580017 C263.472839,456.015015 260.763824,457.054169 257.241425,457.035980 C241.077591,456.952484 224.913086,456.999939 208.748825,457.000000 C177.586792,457.000153 146.424759,457.000732 115.262733,456.999634 C107.121445,456.999329 105.035042,455.395599 105.000999,449.124817 C104.965355,442.559265 106.978073,441.001343 115.503807,441.000183 C123.835869,440.999023 132.167938,440.999939 141.000000,440.999939 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M210.998444,417.000153 C186.170807,417.000061 161.842728,417.010773 137.514694,416.993042 C130.514359,416.987915 126.665665,411.867889 129.469467,405.497375 C130.361252,403.471161 133.579193,401.215637 135.752502,401.195435 C165.910004,400.915100 196.071045,400.965607 226.230942,401.046417 C231.121094,401.059509 233.083664,403.733337 232.998932,409.368042 C232.921509,414.514984 230.865005,416.849884 225.994263,416.971069 C221.164948,417.091248 216.330338,416.999176 210.998444,417.000153 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M364.797363,97.704910 C354.322937,108.192757 344.099365,118.419060 333.450989,129.070267 C340.182373,135.764771 346.649475,142.187607 353.106140,148.620880 C357.354156,152.853470 361.343750,157.387436 365.897583,161.258850 C370.182617,164.901749 369.494812,173.049301 367.156860,175.284821 C364.054932,178.250854 357.078430,177.540771 353.216248,173.703827 C343.762360,164.311676 334.366547,154.861099 324.939575,145.441833 C323.772125,144.275360 322.557465,143.156174 320.608521,141.291687 C316.174255,145.924133 311.886749,150.502441 307.489624,154.972931 C301.415710,161.148209 295.270966,167.254105 289.133881,173.366959 C284.989929,177.494553 278.125275,178.375015 274.860229,175.243683 C271.679443,172.193146 272.316803,165.330841 276.561829,160.992294 C285.530975,151.825531 294.658264,142.813187 303.755219,133.772186 C305.170319,132.365768 306.767456,131.142502 308.699982,129.471832 C307.078339,127.741364 305.910522,126.425888 304.670227,125.182716 C295.140411,115.630798 285.555145,106.133804 276.075226,96.532738 C271.606628,92.007065 271.622955,86.400055 275.876648,82.619972 C279.208740,79.658867 284.745544,80.292557 288.741608,84.259491 C298.080811,93.530586 307.370972,102.851303 316.646271,112.186401 C317.930176,113.478592 319.017365,114.966263 320.457794,116.674324 C322.181274,115.104980 323.412384,114.068321 324.548431,112.936203 C333.749512,103.766907 342.914734,94.561584 352.129089,85.405678 C356.630035,80.933273 361.672668,79.637360 365.330109,81.880753 C370.701630,85.175507 370.583435,91.700768 364.797363,97.704910 z\"/>\n </svg>\n\n </div>\n }\n </div>\n }\n @else if(viewType == 'compact'){\n <div [class.image-picker-label-compact]=\"!previewOnly\" [class.error]=\"required && (filesArray?.length || 0) <= 0\" [class.upload-drag-over]=\"enableDragNDropForUpload && isUploadDragOver\" style=\"height: 42px; width: 42px;\" (dragover)=\"onUploadDragOver($event)\" (dragleave)=\"onUploadDragLeave($event)\" (drop)=\"onUploadDrop($event)\">\n @if(filesArray?.length){\n <div>\n @for(file of filesArray; track file.s3Url; let i = $index; let l = $last;){\n @if(i == 0) {\n <div>\n <div class=\"compact-image-item\" [class.selected]=\"config?.selectionMode ? selectedId == file.id : false\" [class.angular-curser-pointer]=\"!config?.hiddenPreview || config?.selectionMode\" id=\"image-item-{{config.selectorId}}-{{i}}\" [style.height]=\"setHeight('image-item-'+config.selectorId+'-'+i)\" [style.background-image]=\"type == 'file'? '' : 'url('+file.s3Url+')'\" (click)=\"onSelectFile(file)\" [style.background-size]=\"'contain'\">\n <mat-icon *ngIf=\"type == 'file'\" class=\"active angular-mat-icon\">description</mat-icon>\n <span class=\"material-icons cancel\" (click)=\"type == 'file'? deleteFile($event, {}, i, file) : deleteImage($event, {}, i, file)\" *ngIf=\"(!disabled || showDeleteButtonWhenDisabled) && !config.hiddenDeleteBtn && !file?.loading\">highlight_off</span>\n <span class=\"material-icons download\" (click)=\"$event.stopPropagation(); downloadFile(file.s3Url, file.title)\" *ngIf=\"!config.hiddenDownloadBtn && !file?.loading\">download_for_offline</span>\n \n <!-- Loading overlay -->\n <div class=\"loading-img\" *ngIf=\"file?.loading\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"30\"></mat-progress-spinner>\n </div>\n </div>\n \n </div>\n }\n @if(l && (config?.limit || 0) > (filesArray?.length || 0) && !disabled){\n <div class=\"flex flex-col\">\n <!-- Image upload options in grid -->\n <div class=\"image-item uploader\" id=\"image-item-{{config.selectorId}}-{{i}}-{{i}}\" style=\"display: flex; flex-direction: column; gap: 8px; justify-content: center; align-items: center; cursor: pointer;\" (click)=\"loading ? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon class=\"active\">upload_file</mat-icon>\n </div>\n </div>\n }\n }\n </div>\n }\n @else if(!disabled){\n <label style=\"display: flex; gap: 12px; flex-direction: column; justify-content: center; align-items: center; height: 100%; cursor: pointer;\" (click)=\"loading? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon>upload_file</mat-icon>\n </label>\n }\n @else{\n <div class=\"not-found-section\" style=\"display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; cursor: pointer;\">\n <svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" id=\"Layer_1\" x=\"0px\" y=\"0px\" width=\"100%\" viewBox=\"0 0 512 512\" enable-background=\"new 0 0 512 512\" xml:space=\"preserve\">\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M283.000000,513.000000 C188.689560,513.000000 94.879120,513.000000 1.034338,513.000000 C1.034338,342.397858 1.034338,171.795731 1.034338,1.096792 C171.560455,1.096792 342.121002,1.096792 512.840759,1.096792 C512.840759,171.666550 512.840759,342.333252 512.840759,513.000000 C436.462372,513.000000 359.981171,513.000000 283.000000,513.000000 M214.500000,497.000000 C271.998901,496.999969 329.497833,497.019897 386.996674,496.975769 C394.561920,496.969971 401.787231,494.626648 406.395233,488.770782 C412.026520,481.614502 416.644897,473.577057 421.072449,465.572754 C437.390259,436.072754 453.464813,406.438049 469.597321,376.835754 C478.327332,360.816742 487.426758,344.977722 495.530029,328.645905 C501.012421,317.596252 494.173737,304.613556 481.201294,304.861847 C456.377502,305.336975 431.538177,304.999878 406.705292,304.999847 C404.938568,304.999847 403.171844,304.999847 401.000031,304.999847 C401.000031,302.348328 401.003784,300.379181 400.999481,298.410065 C400.935120,269.085632 400.853577,239.761169 400.867645,210.436768 C400.868164,209.340317 401.642242,208.138214 402.315186,207.164581 C408.841400,197.722305 416.441071,188.858276 421.779449,178.792419 C433.290894,157.086823 435.985443,133.380402 431.740631,109.507538 C425.335449,73.484550 405.456726,46.387234 373.051300,29.163473 C356.219269,20.217096 338.040710,16.499708 318.773804,16.813351 C301.234619,17.098873 284.897461,21.133270 269.449402,28.965902 C256.098724,35.735111 244.580948,44.876842 234.887085,56.467457 C224.793365,68.536171 217.675415,82.055397 213.023590,96.957832 C209.186737,109.249481 208.654160,121.911049 208.964157,134.687836 C209.387650,152.143631 214.172058,168.302002 222.741714,183.427231 C224.412766,186.376572 225.987946,189.380234 227.939270,192.969376 C211.045273,192.969376 194.919281,193.199417 178.811310,192.763687 C175.805389,192.682358 172.083817,190.621887 170.038193,188.292847 C160.046188,176.916489 150.561493,165.096268 140.789429,153.524185 C136.493210,148.436600 131.117233,145.037399 124.221672,145.023544 C96.055626,144.966995 67.881424,145.387711 39.727192,144.808304 C28.532793,144.577896 16.775660,156.479080 16.809900,167.857864 C17.130381,274.354980 17.052288,380.853516 16.870073,487.351288 C16.859135,493.743988 19.582472,497.123566 26.503685,497.103851 C88.835381,496.926239 151.167801,497.000000 214.500000,497.000000 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M214.000000,497.000000 C151.167801,497.000000 88.835381,496.926239 26.503685,497.103851 C19.582472,497.123566 16.859135,493.743988 16.870073,487.351288 C17.052288,380.853516 17.130381,274.354980 16.809900,167.857864 C16.775660,156.479080 28.532793,144.577896 39.727192,144.808304 C67.881424,145.387711 96.055626,144.966995 124.221672,145.023544 C131.117233,145.037399 136.493210,148.436600 140.789429,153.524185 C150.561493,165.096268 160.046188,176.916489 170.038193,188.292847 C172.083817,190.621887 175.805389,192.682358 178.811310,192.763687 C194.919281,193.199417 211.045273,192.969376 227.939270,192.969376 C225.987946,189.380234 224.412766,186.376572 222.741714,183.427231 C214.172058,168.302002 209.387650,152.143631 208.964157,134.687836 C208.654160,121.911049 209.186737,109.249481 213.023590,96.957832 C217.675415,82.055397 224.793365,68.536171 234.887085,56.467457 C244.580948,44.876842 256.098724,35.735111 269.449402,28.965902 C284.897461,21.133270 301.234619,17.098873 318.773804,16.813351 C338.040710,16.499708 356.219269,20.217096 373.051300,29.163473 C405.456726,46.387234 425.335449,73.484550 431.740631,109.507538 C435.985443,133.380402 433.290894,157.086823 421.779449,178.792419 C416.441071,188.858276 408.841400,197.722305 402.315186,207.164581 C401.642242,208.138214 400.868164,209.340317 400.867645,210.436768 C400.853577,239.761169 400.935120,269.085632 400.999481,298.410065 C401.003784,300.379181 401.000031,302.348328 401.000031,304.999847 C403.171844,304.999847 404.938568,304.999847 406.705292,304.999847 C431.538177,304.999878 456.377502,305.336975 481.201294,304.861847 C494.173737,304.613556 501.012421,317.596252 495.530029,328.645905 C487.426758,344.977722 478.327332,360.816742 469.597321,376.835754 C453.464813,406.438049 437.390259,436.072754 421.072449,465.572754 C416.644897,473.577057 412.026520,481.614502 406.395233,488.770782 C401.787231,494.626648 394.561920,496.969971 386.996674,496.975769 C329.497833,497.019897 271.998901,496.999969 214.000000,497.000000 M134.597260,321.000000 C127.620270,319.337616 123.942146,323.017517 121.500618,328.932983 C120.498009,331.362091 119.193306,333.673279 117.937233,335.989990 C103.522736,362.575897 89.117485,389.166901 74.653091,415.725647 C67.257729,429.304626 59.689438,442.789490 52.312668,456.378448 C47.974022,464.370728 43.857620,472.483643 39.401810,481.000000 C42.117859,481.000000 44.110912,481.000000 46.103966,481.000000 C144.765793,481.000000 243.427612,481.000031 342.089447,481.000000 C354.922150,481.000000 367.755981,481.100647 380.587189,480.961823 C388.353729,480.877808 394.963684,478.668121 399.028687,471.088623 C414.189636,442.819641 429.540070,414.652191 444.852783,386.464783 C452.465210,372.451935 460.203308,358.507233 467.780884,344.475739 C471.870789,336.902466 475.742279,329.211273 480.010315,321.000000 C364.545990,321.000000 250.059738,321.000000 134.597260,321.000000 M324.557129,242.085190 C327.301666,237.590393 329.664886,232.791306 332.924042,228.707169 C335.118561,225.957092 338.299622,222.950409 341.520416,222.240189 C357.159485,218.791580 371.110535,211.909912 382.881226,201.408112 C405.027954,181.648727 416.679504,157.025070 416.129883,126.765366 C415.719910,104.193253 408.285522,84.336838 393.808441,67.607040 C373.699219,44.368736 348.121246,32.686760 316.779572,33.851212 C294.904785,34.663937 275.751617,42.202290 259.624786,56.188839 C232.720886,79.522171 221.513794,109.436241 227.057266,144.851562 C232.965317,182.596176 262.283539,213.393021 299.806335,221.925400 C304.366974,222.962448 307.440796,225.236969 309.702332,229.227615 C313.147400,235.306534 316.865295,241.230835 320.988098,248.086838 C322.400177,245.685776 323.276184,244.196213 324.557129,242.085190 z\"/>\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M135.085373,321.000000 C250.059738,321.000000 364.545990,321.000000 480.010315,321.000000 C475.742279,329.211273 471.870789,336.902466 467.780884,344.475739 C460.203308,358.507233 452.465210,372.451935 444.852783,386.464783 C429.540070,414.652191 414.189636,442.819641 399.028687,471.088623 C394.963684,478.668121 388.353729,480.877808 380.587189,480.961823 C367.755981,481.100647 354.922150,481.000000 342.089447,481.000000 C243.427612,481.000031 144.765793,481.000000 46.103966,481.000000 C44.110912,481.000000 42.117859,481.000000 39.401810,481.000000 C43.857620,472.483643 47.974022,464.370728 52.312668,456.378448 C59.689438,442.789490 67.257729,429.304626 74.653091,415.725647 C89.117485,389.166901 103.522736,362.575897 117.937233,335.989990 C119.193306,333.673279 120.498009,331.362091 121.500618,328.932983 C123.942146,323.017517 127.620270,319.337616 135.085373,321.000000 M140.500000,440.999939 C132.167938,440.999939 123.835869,440.999023 115.503807,441.000183 C106.978073,441.001343 104.965355,442.559265 105.000999,449.124817 C105.035042,455.395599 107.121445,456.999329 115.262733,456.999634 C146.424759,457.000732 177.586792,457.000153 208.748825,457.000000 C224.913086,456.999939 241.077591,456.952484 257.241425,457.035980 C260.763824,457.054169 263.472839,456.015015 264.705200,452.580017 C267.060974,446.013672 263.486053,441.009705 256.482880,441.007080 C218.155258,440.992767 179.827621,441.000061 140.500000,440.999939 M211.498047,417.000153 C216.330338,416.999176 221.164948,417.091248 225.994263,416.971069 C230.865005,416.849884 232.921509,414.514984 232.998932,409.368042 C233.083664,403.733337 231.121094,401.059509 226.230942,401.046417 C196.071045,400.965607 165.910004,400.915100 135.752502,401.195435 C133.579193,401.215637 130.361252,403.471161 129.469467,405.497375 C126.665665,411.867889 130.514359,416.987915 137.514694,416.993042 C161.842728,417.010773 186.170807,417.000061 211.498047,417.000153 z\"/>\n <path fill=\"none\" opacity=\"1.000000\" stroke=\"none\" d=\" M324.354675,242.395920 C323.276184,244.196213 322.400177,245.685776 320.988098,248.086838 C316.865295,241.230835 313.147400,235.306534 309.702332,229.227615 C307.440796,225.236969 304.366974,222.962448 299.806335,221.925400 C262.283539,213.393021 232.965317,182.596176 227.057266,144.851562 C221.513794,109.436241 232.720886,79.522171 259.624786,56.188839 C275.751617,42.202290 294.904785,34.663937 316.779572,33.851212 C348.121246,32.686760 373.699219,44.368736 393.808441,67.607040 C408.285522,84.336838 415.719910,104.193253 416.129883,126.765366 C416.679504,157.025070 405.027954,181.648727 382.881226,201.408112 C371.110535,211.909912 357.159485,218.791580 341.520416,222.240189 C338.299622,222.950409 335.118561,225.957092 332.924042,228.707169 C329.664886,232.791306 327.301666,237.590393 324.354675,242.395920 M365.048187,97.443359 C370.583435,91.700768 370.701630,85.175507 365.330109,81.880753 C361.672668,79.637360 356.630035,80.933273 352.129089,85.405678 C342.914734,94.561584 333.749512,103.766907 324.548431,112.936203 C323.412384,114.068321 322.181274,115.104980 320.457794,116.674324 C319.017365,114.966263 317.930176,113.478592 316.646271,112.186401 C307.370972,102.851303 298.080811,93.530586 288.741608,84.259491 C284.745544,80.292557 279.208740,79.658867 275.876648,82.619972 C271.622955,86.400055 271.606628,92.007065 276.075226,96.532738 C285.555145,106.133804 295.140411,115.630798 304.670227,125.182716 C305.910522,126.425888 307.078339,127.741364 308.699982,129.471832 C306.767456,131.142502 305.170319,132.365768 303.755219,133.772186 C294.658264,142.813187 285.530975,151.825531 276.561829,160.992294 C272.316803,165.330841 271.679443,172.193146 274.860229,175.243683 C278.125275,178.375015 284.989929,177.494553 289.133881,173.366959 C295.270966,167.254105 301.415710,161.148209 307.489624,154.972931 C311.886749,150.502441 316.174255,145.924133 320.608521,141.291687 C322.557465,143.156174 323.772125,144.275360 324.939575,145.441833 C334.366547,154.861099 343.762360,164.311676 353.216248,173.703827 C357.078430,177.540771 364.054932,178.250854 367.156860,175.284821 C369.494812,173.049301 370.182617,164.901749 365.897583,161.258850 C361.343750,157.387436 357.354156,152.853470 353.106140,148.620880 C346.649475,142.187607 340.182373,135.764771 333.450989,129.070267 C344.099365,118.419060 354.322937,108.192757 365.048187,97.443359 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M141.000000,440.999939 C179.827621,441.000061 218.155258,440.992767 256.482880,441.007080 C263.486053,441.009705 267.060974,446.013672 264.705200,452.580017 C263.472839,456.015015 260.763824,457.054169 257.241425,457.035980 C241.077591,456.952484 224.913086,456.999939 208.748825,457.000000 C177.586792,457.000153 146.424759,457.000732 115.262733,456.999634 C107.121445,456.999329 105.035042,455.395599 105.000999,449.124817 C104.965355,442.559265 106.978073,441.001343 115.503807,441.000183 C123.835869,440.999023 132.167938,440.999939 141.000000,440.999939 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M210.998444,417.000153 C186.170807,417.000061 161.842728,417.010773 137.514694,416.993042 C130.514359,416.987915 126.665665,411.867889 129.469467,405.497375 C130.361252,403.471161 133.579193,401.215637 135.752502,401.195435 C165.910004,400.915100 196.071045,400.965607 226.230942,401.046417 C231.121094,401.059509 233.083664,403.733337 232.998932,409.368042 C232.921509,414.514984 230.865005,416.849884 225.994263,416.971069 C221.164948,417.091248 216.330338,416.999176 210.998444,417.000153 z\"/>\n <path fill=\"var(--angular-primary, #1F6AAD)\" opacity=\"1.000000\" stroke=\"none\" d=\" M364.797363,97.704910 C354.322937,108.192757 344.099365,118.419060 333.450989,129.070267 C340.182373,135.764771 346.649475,142.187607 353.106140,148.620880 C357.354156,152.853470 361.343750,157.387436 365.897583,161.258850 C370.182617,164.901749 369.494812,173.049301 367.156860,175.284821 C364.054932,178.250854 357.078430,177.540771 353.216248,173.703827 C343.762360,164.311676 334.366547,154.861099 324.939575,145.441833 C323.772125,144.275360 322.557465,143.156174 320.608521,141.291687 C316.174255,145.924133 311.886749,150.502441 307.489624,154.972931 C301.415710,161.148209 295.270966,167.254105 289.133881,173.366959 C284.989929,177.494553 278.125275,178.375015 274.860229,175.243683 C271.679443,172.193146 272.316803,165.330841 276.561829,160.992294 C285.530975,151.825531 294.658264,142.813187 303.755219,133.772186 C305.170319,132.365768 306.767456,131.142502 308.699982,129.471832 C307.078339,127.741364 305.910522,126.425888 304.670227,125.182716 C295.140411,115.630798 285.555145,106.133804 276.075226,96.532738 C271.606628,92.007065 271.622955,86.400055 275.876648,82.619972 C279.208740,79.658867 284.745544,80.292557 288.741608,84.259491 C298.080811,93.530586 307.370972,102.851303 316.646271,112.186401 C317.930176,113.478592 319.017365,114.966263 320.457794,116.674324 C322.181274,115.104980 323.412384,114.068321 324.548431,112.936203 C333.749512,103.766907 342.914734,94.561584 352.129089,85.405678 C356.630035,80.933273 361.672668,79.637360 365.330109,81.880753 C370.701630,85.175507 370.583435,91.700768 364.797363,97.704910 z\"/>\n </svg>\n\n </div>\n }\n </div>\n }\n @else if(viewType == 'list'){\n <div [class.error-border]=\"required && (filesArray?.length || 0) <= 0\" [class.upload-drag-over]=\"enableDragNDropForUpload && isUploadDragOver\" [style.height]=\"filesArray?.length ? null : config?.height\" style=\"width: 100%; display: flex; gap: 12px; flex-direction: column; --angular-image-picker-height: {{config?.height ?? 0}}\" (dragover)=\"onUploadDragOver($event)\" (dragleave)=\"onUploadDragLeave($event)\" (drop)=\"onUploadDrop($event)\">\n @if(filesArray?.length){\n @for(file of filesArray; track file.s3Url; let i = $index; let l = $last;){\n <div style=\"position: relative; display: flex; align-items: center; gap: 10px; border-radius: 5px; border: 1px solid black; padding: 5px; height: 56px;\">\n <mat-icon style=\"width: 40px; min-width: 22px;\">description</mat-icon>\n <div style=\"white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%;\">{{file?.title}}</div>\n <div style=\"display: flex; align-items: center; gap: 4px;\" *ngIf=\"!file?.loading\">\n <button type=\"button\" mat-icon-button aria-label=\"Download File\" class=\"angular-icon-btn-sm angular-text-download\" style=\"margin: 0px !important;\" (click)=\"$event.stopPropagation(); downloadFile(file.s3Url, file.title)\" *ngIf=\"!config.hiddenDownloadBtn\">\n <mat-icon>download_for_offline</mat-icon>\n </button>\n <button type=\"button\" mat-icon-button aria-label=\"View File\" class=\"angular-icon-btn-sm angular-text-view\" style=\"margin: 0px !important;\" (click)=\"openFile(file)\" *ngIf=\"!config.hiddenPreview\">\n <mat-icon>visibility</mat-icon>\n </button>\n <button type=\"button\" mat-icon-button aria-label=\"Remove File\" class=\"angular-icon-btn-sm angular-text-cancel\" style=\"margin: 0px !important;\" (click)=\"type == 'file'? deleteFile($event, {}, i, file) : deleteImage($event, {}, i, file)\" *ngIf=\"(!disabled || showDeleteButtonWhenDisabled) && !config.hiddenDeleteBtn\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n \n <!-- Loading overlay for list view -->\n <div class=\"loading-img\" *ngIf=\"file?.loading\" style=\"border-radius: 5px;\">\n <mat-progress-spinner color=\"primary\" mode=\"indeterminate\" [diameter]=\"25\"></mat-progress-spinner>\n </div>\n </div>\n @if(l && (config?.limit || 0) > (filesArray?.length || 0) && !disabled){\n <div style=\"cursor: pointer; display: flex; align-items: center; gap: 4px; border: 1px solid black; border-radius: 5px; padding: 5px; height: 56px;\" (click)=\"loading? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon style=\"width: 40px; min-width: 22px;\">upload_file</mat-icon>\n <div style=\"width: 100%;\">\n <div style=\"display: flex; flex-direction: column; align-items: start; font-size: 14px;\">\n {{label}}\n @if(hint && hint != ''){\n <small>{{hint}}</small>\n }\n </div>\n </div>\n </div>\n }\n }\n }\n @else if(!disabled){\n <div style=\"cursor: pointer; display: flex; align-items: center; gap: 4px; border: 1px solid black; border-radius: 5px; padding: 5px; height: 56px;\" (click)=\"loading? null : type == 'image' && isEnableCapture ? openCameraCapture() : openImageSelector()\">\n <mat-icon style=\"width: 40px; min-width: 22px;\">upload_file</mat-icon>\n <div style=\"width: 100%;\">\n <div style=\"display: flex; flex-direction: column; align-items: start; font-size: 14px;\">\n {{label}}\n @if(hint && hint != ''){\n <small>{{hint}}</small>\n }\n </div>\n </div>\n <!-- Mobile Upload Button for list view -->\n @if(isRemoteUploadAvailable()){\n <button type=\"button\" mat-icon-button class=\"mobile-upload-btn-list\" (click)=\"$event.stopPropagation(); openRemoteUploadDialog()\" title=\"Upload from Mobile\" aria-label=\"Upload from Mobile\">\n <mat-icon>smartphone</mat-icon>\n </button>\n }\n </div>\n }\n </div>\n }\n}\n", styles: [".hover-section{--overlay-display: none;position:relative}.hover-section .hover-section-overlay{display:var(--overlay-display)}.hover-section:hover{--overlay-display: flex !important}.w-full{width:100%!important}.h-full{height:100%!important}.cdk-drop-list-dragging{border:2px solid red!important}.cdk-drag-preview{box-sizing:border-box;border-radius:8px;box-shadow:0 5px 25px -5px #0000004d;opacity:.9}.cdk-drag-placeholder{opacity:.3}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drag-over-highlight .image-item{outline:3px solid #3f51b5!important;outline-offset:2px;box-shadow:0 0 15px #3f51b580!important}.grid{display:grid}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.angular-mat-icon-5x{width:125px!important;height:125px!important;font-size:125px!important}.angular-mat-icon-4x{width:100px!important;height:100px!important;font-size:100px!important}.angular-mat-icon-3x{width:75px!important;height:75px!important;font-size:75px!important}.angular-mat-icon-2x{width:50px!important;height:50px!important;font-size:50px!important}.angular-mat-icon{width:25px!important;height:25px!important;font-size:25px!important;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}.angular-icon-btn-sm{margin-top:7px!important;padding:5px!important;height:36px!important;width:36px!important}.angular-text-download{color:var(--angular-download, var(--mat-sys-primary))}.angular-text-view{color:var(--angular-primary, var(--mat-sys-primary))}.angular-text-cancel{color:var(--angular-cancel, #bb333b)}::ng-deep #upload-img-box{padding:0;border:2px dashed #717171;display:grid;color:#717171;justify-content:center;align-items:center;width:100%;height:160px;background:#eaeaea;cursor:pointer}::ng-deep #upload-img-box input[type=file]{display:none}.preview-img{display:flex;justify-content:center;position:absolute;align-items:center;top:0;width:100%;height:100%;background:#9e9e9e59;opacity:0}.preview-img:hover{opacity:1}.img-box{order:2px solid #b5b5b5!important;position:relative;padding:5px;display:flex;justify-content:center;align-content:center;align-items:center}.loading-img{display:flex;justify-content:center;position:absolute;align-items:center;top:0;left:0;width:100%;height:100%;background:#9e9e9e59}.image-picker-label{border:none;border-radius:4px;padding:0;box-sizing:border-box;height:var(--angular-image-picker-height, 130px)}.image-picker-label:has(.image-list){border:2px solid #e3e8ef;padding:16px;height:auto}.image-picker-label.error{border:2px dashed var(--angular-error, #a00404);padding:16px}.image-picker-label-compact{border:1px dashed rgba(0,0,0,.38);border-radius:4px;padding:4px;box-sizing:border-box}.image-picker-label-compact.error{border:2px dashed var(--angular-error, #a00404)}.compact-image-item{width:42px;height:42px;position:relative;display:flex;align-items:center;justify-content:center}.compact-image-item .cancel{color:var(--angular-cancel, #bb333b);background-color:#fff;border-radius:20px;position:absolute;top:-10px;right:-10px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.compact-image-item .download{color:var(--angular-download, var(--mat-sys-primary));background-color:#fff;border-radius:20px;position:absolute;top:-10px;left:-10px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.image-list .image-item{aspect-ratio:1;height:var(--angular-image-picker-height, 130px);border-radius:5px;background-position:center;background-size:cover;position:relative;display:flex;justify-content:center;align-items:center;border:1px solid rgba(0,0,0,.38);width:100%}.image-list .image-item.uploader{border:2px dashed #d0d5dd!important;cursor:pointer;transition:all .3s ease;background:#fff}.image-list .image-item.uploader:hover{border-color:var(--angular-primary, #1F6AAD)!important;background:linear-gradient(to bottom,#fff,#f8f9fc);box-shadow:0 2px 8px #1f6aad14}.image-list .image-item:hover .mat-icon{display:unset!important}.image-list .image-item.selected{border:3px solid var(--angular-item-selected, var(--mat-sys-primary))!important;width:calc(100% - 4px)}.image-list .image-item .mat-icon{display:none}.image-list .image-item .mat-icon.active{display:unset!important}.image-list .image-item .cancel{color:var(--angular-cancel, #bb333b);background-color:#fff;border-radius:20px;position:absolute;top:-10px;right:-10px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.image-list .image-item .download{color:var(--angular-download, var(--mat-sys-primary));background-color:#fff;border-radius:20px;position:absolute;top:-10px;left:-10px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.image-list .image-item .drag{color:var(--angular-primary, var(--mat-sys-primary));background-color:#fff;border-radius:20px;position:absolute;top:-10px;left:30px;cursor:pointer;box-shadow:0 0 5px #9e9e9e}.image-list .image-item .shedded{background-color:#9e9e9ecc;border-radius:5px}.error-border{border:1px solid var(--angular-error-color, #a00404)!important;border-radius:5px!important}.download{color:var(--angular-download, var(--mat-sys-primary));position:absolute;top:-10px;left:-10px;cursor:pointer}.angular-curser-pointer{cursor:pointer}::ng-deep .angular-confirmation-dialog mat-dialog-container .mat-mdc-dialog-title{padding:8px 16px;display:flex;gap:10px;justify-content:start;width:100%;align-items:center}::ng-deep .angular-confirmation-dialog mat-dialog-container .mat-mdc-dialog-title:before{content:unset}::ng-deep .angular-confirmation-dialog mat-dialog-container .mat-mdc-dialog-content{padding-top:15px;font-size:14px}::ng-deep .angular-confirmation-dialog mat-dialog-container .mat-mdc-dialog-actions{border-top:1px solid rgba(0,0,0,.12);justify-content:end}.not-found-section svg{margin:auto;height:calc(var(--angular-image-picker-height) - 45px);max-height:150px}.image-description{width:100%;margin-top:.5rem;position:relative}.image-description-text{padding:.5rem;background-color:#f9fafb;border:1px solid #e5e7eb;border-radius:.375rem;font-size:.875rem;color:#4b5563}.image-description-text .anchor{color:#00f;cursor:pointer}.image-description-text .anchor:hover{text-decoration:underline}.edit-description-btn{color:#4b5563;border-radius:20px;position:absolute;right:.5rem;top:.5rem;cursor:pointer}.image-description-edit{display:flex;flex-direction:column}.image-description-edit textarea,.image-description-edit input{font-size:.875rem;line-height:1.25rem;padding:.5rem;--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1));border-width:1px;border-radius:.375rem;resize:none}.description-actions{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.5rem}.description-action-btn{padding:.25rem .5rem;border-radius:.25rem;font-size:.75rem;font-weight:500;background-image:none;cursor:pointer;box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}.description-cancel-btn{background-color:#f3f4f6;color:var(--angular-cancel);border:1px solid var(--angular-cancel)}.description-save-btn{background-color:var(--angular-primary, var(--mat-sys-primary));color:#fff}@media (max-width: 575.98px){.image-picker-label{padding:15px}}@media (min-width: 576px) and (max-width: 767.98px){.image-picker-label{padding:15px}}.main-upload-wrapper{position:relative;width:100%;overflow:hidden}.mobile-upload-overlay{position:absolute;top:13px;left:50%;transform:translate(-50%);z-index:10;padding:12px;display:flex;justify-content:center;align-items:center;box-sizing:border-box;pointer-events:all;width:100%}.mobile-upload-overlay-grid{padding:0;display:flex;justify-content:center;align-items:center;box-sizing:border-box;pointer-events:all}.upload-content{display:flex;flex-direction:column;gap:12px;align-items:center;justify-content:center;pointer-events:all;width:100%}.upload-content.with-mobile{margin-top:70px}.mobile-upload-section{padding:16px;background:linear-gradient(145deg,#fff,#f8f9fc);border:1px solid #e3e8ef;border-radius:12px;margin-bottom:16px;display:flex;justify-content:center;align-items:center;box-shadow:0 1px 3px #0000000d;box-sizing:border-box}.mobile-upload-section.grid-mobile-section{margin-bottom:10px;padding:10px}.mobile-upload-btn{display:flex;align-items:center;gap:8px;padding:12px 20px;background:linear-gradient(135deg,var(--angular-primary, #1F6AAD) 0%,#1a5a94 100%);color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 8px #1f6aad33;letter-spacing:.3px;box-sizing:border-box}.mobile-upload-btn.compact{padding:8px 14px;font-size:13px}.mobile-upload-btn:hover{background:linear-gradient(135deg,#175080,#134066);transform:translateY(-2px);box-shadow:0 4px 12px #1f6aad4d}.mobile-upload-btn:active{transform:translateY(0);box-shadow:0 2px 4px #1f6aad33}.mobile-upload-btn.connected{background:linear-gradient(135deg,#4caf50,#388e3c);box-shadow:0 2px 8px #4caf5033}.mobile-upload-btn.connected:hover{background:linear-gradient(135deg,#388e3c,#2e7d32);box-shadow:0 4px 12px #4caf504d}.mobile-upload-btn mat-icon{font-size:20px;width:20px;height:20px}.mobile-upload-btn span{white-space:nowrap;font-weight:600}.mobile-upload-actions{display:flex;align-items:center;gap:8px}.main-upload-label{background:#fff;border:2px dashed #d0d5dd;border-radius:12px;padding:24px;transition:all .3s ease;height:calc(var(--angular-image-picker-height, 130px) + var(--angular-image-upload-config-height, 0px));box-sizing:border-box;position:relative;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;overflow:hidden}.main-upload-label:hover{border-color:var(--angular-primary, #1F6AAD);background:linear-gradient(to bottom,#fff,#f8f9fc);box-shadow:0 2px 8px #1f6aad14}.view-connection-btn{display:flex;align-items:center;justify-content:center;padding:5px;background:#fff;color:var(--angular-primary, #1F6AAD);border:2px solid var(--angular-primary, #1F6AAD);border-radius:8px;cursor:pointer;transition:all .3s cubic-bezier(.4,0,.2,1);box-shadow:0 2px 4px #1f6aad1a}.view-connection-btn.compact{padding:8px}.view-connection-btn:hover{background:var(--angular-primary, #1F6AAD);color:#fff;transform:translateY(-2px);box-shadow:0 4px 12px #1f6aad40}.view-connection-btn:active{transform:translateY(0);box-shadow:0 2px 4px #1f6aad26}.view-connection-btn mat-icon{font-size:20px;width:20px;height:20px}.disconnect-btn{display:flex;align-items:center;justify-content:center;width:32px;height:32px;background-color:#f44336;color:#fff;border:none;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.disconnect-btn:hover{background-color:#d32f2f}.disconnect-btn mat-icon{font-size:18px;width:18px;height:18px}.mobile-waiting-state{display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px;width:100%}.mobile-waiting-state .waiting-text{font-size:12px;color:#666}.mobile-waiting-state .cancel-mobile-btn{position:absolute;top:4px;right:4px;display:flex;align-items:center;justify-content:center;width:24px;height:24px;background-color:transparent;color:#666;border:none;border-radius:50%;cursor:pointer;transition:background-color .2s ease}.mobile-waiting-state .cancel-mobile-btn:hover{background-color:#0000001a}.mobile-waiting-state .cancel-mobile-btn mat-icon{font-size:16px;width:16px;height:16px}.cancel-mobile-btn-text{margin-top:8px;padding:6px 16px;background-color:transparent;color:var(--angular-primary, #1F6AAD);border:1px solid var(--angular-primary, #1F6AAD);border-radius:4px;cursor:pointer;font-size:12px;transition:background-color .2s ease}.cancel-mobile-btn-text:hover{background-color:#1f6aad1a}.mobile-upload-btn-list{display:flex;align-items:center;justify-content:center;width:40px;height:40px;background-color:var(--angular-primary, #1F6AAD);color:#fff;border:none;border-radius:50%;cursor:pointer;transition:background-color .2s ease;flex-shrink:0}.mobile-upload-btn-list:hover{background-color:var(--angular-primary-dark, #175080)}.mobile-upload-btn-list mat-icon{font-size:20px;width:20px;height:20px}.view-connection-btn{display:flex;align-items:center;justify-content:center;width:36px;height:36px;background-color:#e8f5e9;color:#4caf50;border:1px solid #4caf50;border-radius:4px;cursor:pointer;transition:all .2s ease}.view-connection-btn:hover{background-color:#c8e6c9}.view-connection-btn mat-icon{font-size:18px;width:18px;height:18px}.pending-files-container{display:flex;flex-direction:column;gap:8px;width:100%;max-height:200px;overflow-y:auto;padding:8px;background-color:#fff8e1;border-radius:4px;border:1px solid #ffcc80}.pending-files-header{display:flex;align-items:center;gap:6px;font-size:12px;color:#e65100;font-weight:500}.pending-files-header mat-icon{font-size:16px;width:16px;height:16px}.pending-files-list{display:flex;flex-direction:column;gap:6px}.pending-file-item{display:flex;align-items:center;gap:8px;padding:6px;background-color:#fff;border-radius:4px;border:1px solid #e0e0e0}.pending-file-preview{width:36px;height:36px;min-width:36px;border-radius:4px;background-size:cover;background-position:center;background-color:#f5f5f5;display:flex;align-items:center;justify-content:center}.pending-file-preview mat-icon{font-size:20px;color:#757575}.pending-file-name{flex:1;font-size:12px;color:#424242;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pending-file-actions{display:flex;gap:4px}.pending-file-actions .accept-btn{width:28px;height:28px;color:#4caf50}.pending-file-actions .accept-btn:hover{background-color:#4caf501a}.pending-file-actions .reject-btn{width:28px;height:28px;color:#f44336}.pending-file-actions .reject-btn:hover{background-color:#f443361a}.pending-file-actions mat-icon{font-size:18px;width:18px;height:18px}::ng-deep .camera-capture-dialog{padding:0!important;max-width:100vw!important;max-height:100vh!important}::ng-deep .camera-capture-dialog .mat-mdc-dialog-container{padding:0!important;background:transparent!important;box-shadow:none!important}.image-picker-label.upload-drag-over,.image-picker-label-compact.upload-drag-over,.error-border.upload-drag-over{outline:2px dashed var(--angular-primary, #1F6AAD);outline-offset:2px;background-color:#1f6aad0f}\n"] }]
3500
+ }], ctorParameters: () => [{ type: i1$2.MatDialog }, { type: AngularHelperService }, { type: i3$4.BreakpointObserver }, { type: AngularRemoteUploadService }], propDecorators: { urlConfig: [{
3501
+ type: Input,
3502
+ args: [{ required: true }]
3503
+ }], entityType: [{
3504
+ type: Input
3505
+ }], entityId: [{
3506
+ type: Input
3507
+ }], viewType: [{
3508
+ type: Input
3509
+ }], type: [{
3510
+ type: Input
3511
+ }], label: [{
3512
+ type: Input
3513
+ }], disabled: [{
3514
+ type: Input
3515
+ }], data: [{
3516
+ type: Input
3517
+ }], hint: [{
3518
+ type: Input
3519
+ }], accept: [{
3520
+ type: Input
3521
+ }], isValidateMimeType: [{
3522
+ type: Input
3523
+ }], selectedId: [{
3524
+ type: Input
3525
+ }], options: [{
3526
+ type: Input
3527
+ }], required: [{
3528
+ type: Input
3529
+ }], previewOnly: [{
3530
+ type: Input
3531
+ }], previewInFlex: [{
3532
+ type: Input
3533
+ }], imageItemClass: [{
3534
+ type: Input
3535
+ }], isAddUploadedFileInLastNode: [{
3536
+ type: Input
3537
+ }], isEnableDeleteConfirmation: [{
3538
+ type: Input
3539
+ }], isEnableCapture: [{
3540
+ type: Input
3541
+ }], deleteConfirmationMsg: [{
3542
+ type: Input
3543
+ }], dialogConfig: [{
3544
+ type: Input
3545
+ }], enableDragNDropForUpload: [{
3546
+ type: Input
3547
+ }], remoteUploadConfig: [{
3548
+ type: Input
3549
+ }], uploadInProgress: [{
3550
+ type: Output
3551
+ }], onUploaded: [{
3552
+ type: Output
3553
+ }], onFileSelect: [{
3554
+ type: Output
3555
+ }], onFileRemoved: [{
3556
+ type: Output
3557
+ }], onError: [{
3558
+ type: Output
3559
+ }], onRemoteUpload: [{
3560
+ type: Output
3561
+ }], isShowImageSequence: [{
3562
+ type: Input
3563
+ }], enableDragNDrop: [{
3564
+ type: Input
3565
+ }], editTagButtonWhenDisabled: [{
3566
+ type: Input
3567
+ }], showDeleteButtonWhenDisabled: [{
3568
+ type: Input
3569
+ }], dataSequenceChange: [{
3570
+ type: Output
3571
+ }] } });
3572
+
3573
+ const uiImports = [
3574
+ MatTooltipModule,
3575
+ MatIconModule,
3576
+ MatSnackBarModule,
3577
+ MatProgressSpinnerModule,
3578
+ MatInputModule,
3579
+ MatButtonModule,
3580
+ MatDialogModule,
3581
+ MatDividerModule,
3582
+ ];
3583
+ class AngularImageAndFilePickerModule {
3584
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
3585
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerModule, declarations: [AngularImageAndFilePickerComponent,
3586
+ AngularPreviewImageComponent,
3587
+ AngularFileViewerComponent,
3588
+ AngularExcelFileViewerComponent,
3589
+ AngularPdfViewerComponent,
3590
+ AngularVideoComponent,
3591
+ AngularErrorDialogComponent,
3592
+ AngularConfirmationDialogComponent,
3593
+ AngularQrCodeDialogComponent,
3594
+ AngularViewConnectionDialogComponent,
3595
+ TimeAgoPipe], imports: [CommonModule,
3596
+ HttpClientModule,
3597
+ NgxExtendedPdfViewerModule,
3598
+ FormsModule,
3599
+ ReactiveFormsModule,
3600
+ QRCodeComponent, MatTooltipModule,
3601
+ MatIconModule,
3602
+ MatSnackBarModule,
3603
+ MatProgressSpinnerModule,
3604
+ MatInputModule,
3605
+ MatButtonModule,
3606
+ MatDialogModule,
3607
+ MatDividerModule, DragDropModule], exports: [AngularImageAndFilePickerComponent,
3608
+ AngularQrCodeDialogComponent] });
3609
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerModule, imports: [CommonModule,
3610
+ HttpClientModule,
3611
+ NgxExtendedPdfViewerModule,
3612
+ FormsModule,
3613
+ ReactiveFormsModule,
3614
+ QRCodeComponent, uiImports, DragDropModule] });
3615
+ }
3616
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: AngularImageAndFilePickerModule, decorators: [{
3617
+ type: NgModule,
3618
+ args: [{
3619
+ declarations: [
3620
+ AngularImageAndFilePickerComponent,
3621
+ AngularPreviewImageComponent,
3622
+ AngularFileViewerComponent,
3623
+ AngularExcelFileViewerComponent,
3624
+ AngularPdfViewerComponent,
3625
+ AngularVideoComponent,
3626
+ AngularErrorDialogComponent,
3627
+ AngularConfirmationDialogComponent,
3628
+ AngularQrCodeDialogComponent,
3629
+ AngularViewConnectionDialogComponent,
3630
+ TimeAgoPipe
3631
+ ],
3632
+ imports: [
3633
+ CommonModule,
3634
+ HttpClientModule,
3635
+ NgxExtendedPdfViewerModule,
3636
+ FormsModule,
3637
+ ReactiveFormsModule,
3638
+ QRCodeComponent,
3639
+ ...uiImports,
3640
+ DragDropModule
3641
+ ],
3642
+ exports: [
3643
+ AngularImageAndFilePickerComponent,
3644
+ AngularQrCodeDialogComponent
3645
+ ]
3646
+ }]
3647
+ }] });
3648
+
3649
+ /*
3650
+ * Public API Surface of angular-image-and-file-picker
3651
+ */
3652
+
3653
+ /**
3654
+ * Generated bundle index. Do not edit.
3655
+ */
3656
+
3657
+ export { AngularCameraCaptureDialogComponent, AngularImageAndFilePickerComponent, AngularImageAndFilePickerModule, AngularImageAndFilePickerService, AngularQrCodeDialogComponent, AngularRemoteUploadService };
3658
+ //# sourceMappingURL=servicemind.tis-angular-image-and-file-picker.mjs.map