valtech-components 2.0.679 → 2.0.681
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/lib/services/auth/auth.service.mjs +2 -11
- package/esm2022/lib/services/auth/types.mjs +1 -1
- package/esm2022/lib/version.mjs +2 -2
- package/esm2022/public-api.mjs +1 -7
- package/fesm2022/valtech-components.mjs +4 -783
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/services/auth/auth.service.d.ts +1 -6
- package/lib/services/auth/types.d.ts +0 -18
- package/lib/version.d.ts +1 -1
- package/package.json +2 -6
- package/public-api.d.ts +0 -4
- package/esm2022/lib/components/molecules/image-crop/image-crop.component.mjs +0 -174
- package/esm2022/lib/components/molecules/image-crop/index.mjs +0 -2
- package/esm2022/lib/components/organisms/avatar-upload/avatar-upload.component.mjs +0 -345
- package/esm2022/lib/components/organisms/avatar-upload/types.mjs +0 -15
- package/esm2022/lib/services/image/image.service.mjs +0 -244
- package/esm2022/lib/services/image/index.mjs +0 -3
- package/esm2022/lib/services/image/types.mjs +0 -13
- package/lib/components/molecules/image-crop/image-crop.component.d.ts +0 -59
- package/lib/components/molecules/image-crop/index.d.ts +0 -1
- package/lib/components/organisms/avatar-upload/avatar-upload.component.d.ts +0 -82
- package/lib/components/organisms/avatar-upload/types.d.ts +0 -62
- package/lib/services/image/image.service.d.ts +0 -76
- package/lib/services/image/index.d.ts +0 -2
- package/lib/services/image/types.d.ts +0 -74
- package/src/lib/services/firebase/firebase-messaging-sw.js +0 -134
|
@@ -5,7 +5,7 @@ import { IonAvatar, IonCard, IonIcon, IonButton, IonSpinner, IonText, IonModal,
|
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
6
6
|
import { CommonModule, NgStyle, NgFor, isPlatformBrowser, NgClass } from '@angular/common';
|
|
7
7
|
import { addIcons } from 'ionicons';
|
|
8
|
-
import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, shuffleOutline, logoWhatsapp, paperPlaneOutline, mailOutline, trophyOutline, ticketOutline, giftOutline, personOutline, ellipsisVertical, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline,
|
|
8
|
+
import { addOutline, addCircleOutline, alertOutline, alertCircleOutline, arrowBackOutline, arrowForwardOutline, arrowDownOutline, settings, settingsOutline, checkmarkCircleOutline, ellipsisHorizontalOutline, notifications, notificationsOutline, openOutline, closeOutline, chatbubblesOutline, shareOutline, heart, heartOutline, home, homeOutline, eyeOffOutline, eyeOutline, scanOutline, chevronDownOutline, chevronForwardOutline, checkmarkOutline, clipboardOutline, copyOutline, filterOutline, locationOutline, calendarOutline, businessOutline, logoTwitter, logoInstagram, logoLinkedin, logoYoutube, logoTiktok, logoFacebook, logoGoogle, createOutline, trashOutline, playOutline, refreshOutline, documentTextOutline, lockClosedOutline, informationCircleOutline, logoNpm, removeOutline, add, close, share, create, trash, star, camera, mic, send, downloadOutline, chevronDown, language, globeOutline, checkmark, list, grid, apps, menu, search, person, helpCircle, informationCircle, documentText, mail, calendar, folder, chevronForward, ellipsisHorizontal, chevronBack, playBack, playForward, ellipse, starOutline, starHalf, heartHalf, checkmarkCircle, timeOutline, flag, trendingUp, trendingDown, remove, analytics, people, cash, cart, eye, chatbubbleOutline, thumbsUpOutline, thumbsUp, happyOutline, happy, sadOutline, sad, chevronUp, pin, pencil, callOutline, shuffleOutline, logoWhatsapp, paperPlaneOutline, mailOutline, trophyOutline, ticketOutline, giftOutline, personOutline, ellipsisVertical, closeCircle, alertCircle, logoApple, logoMicrosoft, linkOutline, unlinkOutline, chevronBackOutline, sendOutline, chatbubbleEllipsesOutline, swapVerticalOutline, chevronUpOutline, documentOutline, searchOutline, cartOutline, chatbubble, compass, compassOutline, gridOutline, listOutline, folderOutline, documents, documentsOutline, statsChart, statsChartOutline, bugOutline, bulbOutline, closeCircleOutline, menuOutline } from 'ionicons/icons';
|
|
9
9
|
import * as i1$1 from '@angular/router';
|
|
10
10
|
import { Router, NavigationEnd, RouterLink, RouterOutlet, RouterModule } from '@angular/router';
|
|
11
11
|
import { Browser } from '@capacitor/browser';
|
|
@@ -39,7 +39,6 @@ import * as i1$7 from '@angular/fire/storage';
|
|
|
39
39
|
import { provideStorage, getStorage, connectStorageEmulator, ref, uploadBytesResumable, getDownloadURL, getMetadata, deleteObject, listAll } from '@angular/fire/storage';
|
|
40
40
|
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
41
41
|
import { filter, map as map$1, catchError as catchError$1, tap, switchMap as switchMap$1, finalize, take, debounceTime as debounceTime$1, takeUntil as takeUntil$1 } from 'rxjs/operators';
|
|
42
|
-
import { ImageCropperComponent } from 'ngx-image-cropper';
|
|
43
42
|
import * as i1$8 from '@angular/common/http';
|
|
44
43
|
import { provideHttpClient, withInterceptors, HttpClient } from '@angular/common/http';
|
|
45
44
|
import { Capacitor } from '@capacitor/core';
|
|
@@ -50,7 +49,7 @@ import 'prismjs/components/prism-json';
|
|
|
50
49
|
* Current version of valtech-components.
|
|
51
50
|
* This is automatically updated during the publish process.
|
|
52
51
|
*/
|
|
53
|
-
const VERSION = '2.0.
|
|
52
|
+
const VERSION = '2.0.681';
|
|
54
53
|
|
|
55
54
|
/**
|
|
56
55
|
* Servicio para gestionar presets de componentes.
|
|
@@ -21197,174 +21196,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
21197
21196
|
type: Input
|
|
21198
21197
|
}] } });
|
|
21199
21198
|
|
|
21200
|
-
/**
|
|
21201
|
-
* ImageCropComponent
|
|
21202
|
-
*
|
|
21203
|
-
* A modal-ready component for cropping images with a specified aspect ratio.
|
|
21204
|
-
* Uses ngx-image-cropper internally and provides a simple interface.
|
|
21205
|
-
*
|
|
21206
|
-
* @example Inside an ion-modal
|
|
21207
|
-
* ```html
|
|
21208
|
-
* <ion-modal [isOpen]="showCropModal">
|
|
21209
|
-
* <ng-template>
|
|
21210
|
-
* <val-image-crop
|
|
21211
|
-
* [image]="selectedFile"
|
|
21212
|
-
* [aspectRatio]="1"
|
|
21213
|
-
* [roundCropper]="true"
|
|
21214
|
-
* (cropComplete)="onCropComplete($event)"
|
|
21215
|
-
* (cancel)="showCropModal = false"
|
|
21216
|
-
* />
|
|
21217
|
-
* </ng-template>
|
|
21218
|
-
* </ion-modal>
|
|
21219
|
-
* ```
|
|
21220
|
-
*/
|
|
21221
|
-
class ImageCropComponent {
|
|
21222
|
-
constructor() {
|
|
21223
|
-
this.i18n = inject(I18nService);
|
|
21224
|
-
/** Image file to crop */
|
|
21225
|
-
this.image = input.required();
|
|
21226
|
-
/** Aspect ratio (1 for square, 16/9 for widescreen, etc.) */
|
|
21227
|
-
this.aspectRatio = input(1);
|
|
21228
|
-
/** Use round cropper (for avatars) */
|
|
21229
|
-
this.roundCropper = input(true);
|
|
21230
|
-
/** Resize output to specific width (0 = no resize) */
|
|
21231
|
-
this.resizeToWidth = input(0);
|
|
21232
|
-
/** i18n namespace for labels */
|
|
21233
|
-
this.i18nNamespace = input('ImageCrop');
|
|
21234
|
-
/** Emitted when crop is confirmed with the cropped blob */
|
|
21235
|
-
this.cropComplete = new EventEmitter();
|
|
21236
|
-
/** Emitted when user cancels the crop */
|
|
21237
|
-
this.cancel = new EventEmitter();
|
|
21238
|
-
/** Emitted when image fails to load */
|
|
21239
|
-
this.loadFailed = new EventEmitter();
|
|
21240
|
-
/** Internal signal for cropped blob */
|
|
21241
|
-
this.croppedBlob = signal(null);
|
|
21242
|
-
/** Computed text for cancel button */
|
|
21243
|
-
this.cancelText = computed(() => {
|
|
21244
|
-
this.i18n.lang(); // Track language changes
|
|
21245
|
-
return this.i18n.t('cancel', 'Common') || 'Cancelar';
|
|
21246
|
-
});
|
|
21247
|
-
/** Computed text for confirm button */
|
|
21248
|
-
this.confirmText = computed(() => {
|
|
21249
|
-
this.i18n.lang();
|
|
21250
|
-
return this.i18n.t('confirm', 'Common') || 'Confirmar';
|
|
21251
|
-
});
|
|
21252
|
-
/** Computed text for title */
|
|
21253
|
-
this.titleText = computed(() => {
|
|
21254
|
-
this.i18n.lang();
|
|
21255
|
-
return this.i18n.t('cropImage', this.i18nNamespace()) || 'Recortar imagen';
|
|
21256
|
-
});
|
|
21257
|
-
}
|
|
21258
|
-
/** Handle crop event from ngx-image-cropper */
|
|
21259
|
-
onImageCropped(event) {
|
|
21260
|
-
if (event.blob) {
|
|
21261
|
-
this.croppedBlob.set(event.blob);
|
|
21262
|
-
}
|
|
21263
|
-
}
|
|
21264
|
-
/** Confirm and emit the cropped blob */
|
|
21265
|
-
confirmCrop() {
|
|
21266
|
-
const blob = this.croppedBlob();
|
|
21267
|
-
if (blob) {
|
|
21268
|
-
this.cropComplete.emit(blob);
|
|
21269
|
-
}
|
|
21270
|
-
}
|
|
21271
|
-
/** Handle load failure */
|
|
21272
|
-
onLoadFailed() {
|
|
21273
|
-
this.loadFailed.emit();
|
|
21274
|
-
}
|
|
21275
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageCropComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
21276
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "18.2.14", type: ImageCropComponent, isStandalone: true, selector: "val-image-crop", inputs: { image: { classPropertyName: "image", publicName: "image", isSignal: true, isRequired: true, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, roundCropper: { classPropertyName: "roundCropper", publicName: "roundCropper", isSignal: true, isRequired: false, transformFunction: null }, resizeToWidth: { classPropertyName: "resizeToWidth", publicName: "resizeToWidth", isSignal: true, isRequired: false, transformFunction: null }, i18nNamespace: { classPropertyName: "i18nNamespace", publicName: "i18nNamespace", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { cropComplete: "cropComplete", cancel: "cancel", loadFailed: "loadFailed" }, ngImport: i0, template: `
|
|
21277
|
-
<ion-header>
|
|
21278
|
-
<ion-toolbar>
|
|
21279
|
-
<ion-buttons slot="start">
|
|
21280
|
-
<ion-button (click)="cancel.emit()" color="medium">
|
|
21281
|
-
{{ cancelText() }}
|
|
21282
|
-
</ion-button>
|
|
21283
|
-
</ion-buttons>
|
|
21284
|
-
<ion-title>{{ titleText() }}</ion-title>
|
|
21285
|
-
<ion-buttons slot="end">
|
|
21286
|
-
<ion-button
|
|
21287
|
-
(click)="confirmCrop()"
|
|
21288
|
-
color="primary"
|
|
21289
|
-
[strong]="true"
|
|
21290
|
-
[disabled]="!croppedBlob()"
|
|
21291
|
-
>
|
|
21292
|
-
{{ confirmText() }}
|
|
21293
|
-
</ion-button>
|
|
21294
|
-
</ion-buttons>
|
|
21295
|
-
</ion-toolbar>
|
|
21296
|
-
</ion-header>
|
|
21297
|
-
|
|
21298
|
-
<ion-content class="image-crop-content">
|
|
21299
|
-
<image-cropper
|
|
21300
|
-
[imageFile]="image()"
|
|
21301
|
-
[aspectRatio]="aspectRatio()"
|
|
21302
|
-
[maintainAspectRatio]="true"
|
|
21303
|
-
[roundCropper]="roundCropper()"
|
|
21304
|
-
[resizeToWidth]="resizeToWidth()"
|
|
21305
|
-
format="jpeg"
|
|
21306
|
-
outputType="blob"
|
|
21307
|
-
(imageCropped)="onImageCropped($event)"
|
|
21308
|
-
(loadImageFailed)="onLoadFailed()"
|
|
21309
|
-
/>
|
|
21310
|
-
</ion-content>
|
|
21311
|
-
`, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%}.image-crop-content{--background: var(--ion-color-dark)}.image-crop-content::part(scroll){display:flex;flex-direction:column}image-cropper{--cropper-outline-color: rgba(255, 255, 255, .3);--cropper-background-color: var(--ion-color-dark);flex:1;height:100%;max-height:calc(100vh - 56px)}::ng-deep .ngx-ic-component{height:100%!important}::ng-deep .ngx-ic-source-image{max-height:100%!important}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: ImageCropperComponent, selector: "image-cropper", inputs: ["imageChangedEvent", "imageURL", "imageBase64", "imageFile", "imageAltText", "options", "cropperFrameAriaLabel", "output", "format", "autoCrop", "cropper", "transform", "maintainAspectRatio", "aspectRatio", "resetCropOnAspectRatioChange", "resizeToWidth", "resizeToHeight", "cropperMinWidth", "cropperMinHeight", "cropperMaxHeight", "cropperMaxWidth", "cropperStaticWidth", "cropperStaticHeight", "canvasRotation", "initialStepSize", "roundCropper", "onlyScaleDown", "imageQuality", "backgroundColor", "containWithinAspectRatio", "hideResizeSquares", "allowMoveImage", "checkImageType", "alignImage", "disabled", "hidden"], outputs: ["imageCropped", "startCropImage", "imageLoaded", "cropperReady", "loadImageFailed", "transformChange", "cropperChange"] }] }); }
|
|
21312
|
-
}
|
|
21313
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageCropComponent, decorators: [{
|
|
21314
|
-
type: Component,
|
|
21315
|
-
args: [{ selector: 'val-image-crop', standalone: true, imports: [
|
|
21316
|
-
CommonModule,
|
|
21317
|
-
IonHeader,
|
|
21318
|
-
IonToolbar,
|
|
21319
|
-
IonTitle,
|
|
21320
|
-
IonButtons,
|
|
21321
|
-
IonButton,
|
|
21322
|
-
IonContent,
|
|
21323
|
-
ImageCropperComponent,
|
|
21324
|
-
], template: `
|
|
21325
|
-
<ion-header>
|
|
21326
|
-
<ion-toolbar>
|
|
21327
|
-
<ion-buttons slot="start">
|
|
21328
|
-
<ion-button (click)="cancel.emit()" color="medium">
|
|
21329
|
-
{{ cancelText() }}
|
|
21330
|
-
</ion-button>
|
|
21331
|
-
</ion-buttons>
|
|
21332
|
-
<ion-title>{{ titleText() }}</ion-title>
|
|
21333
|
-
<ion-buttons slot="end">
|
|
21334
|
-
<ion-button
|
|
21335
|
-
(click)="confirmCrop()"
|
|
21336
|
-
color="primary"
|
|
21337
|
-
[strong]="true"
|
|
21338
|
-
[disabled]="!croppedBlob()"
|
|
21339
|
-
>
|
|
21340
|
-
{{ confirmText() }}
|
|
21341
|
-
</ion-button>
|
|
21342
|
-
</ion-buttons>
|
|
21343
|
-
</ion-toolbar>
|
|
21344
|
-
</ion-header>
|
|
21345
|
-
|
|
21346
|
-
<ion-content class="image-crop-content">
|
|
21347
|
-
<image-cropper
|
|
21348
|
-
[imageFile]="image()"
|
|
21349
|
-
[aspectRatio]="aspectRatio()"
|
|
21350
|
-
[maintainAspectRatio]="true"
|
|
21351
|
-
[roundCropper]="roundCropper()"
|
|
21352
|
-
[resizeToWidth]="resizeToWidth()"
|
|
21353
|
-
format="jpeg"
|
|
21354
|
-
outputType="blob"
|
|
21355
|
-
(imageCropped)="onImageCropped($event)"
|
|
21356
|
-
(loadImageFailed)="onLoadFailed()"
|
|
21357
|
-
/>
|
|
21358
|
-
</ion-content>
|
|
21359
|
-
`, styles: [":host{display:flex;flex-direction:column;height:100%}.image-crop-content{--background: var(--ion-color-dark)}.image-crop-content::part(scroll){display:flex;flex-direction:column}image-cropper{--cropper-outline-color: rgba(255, 255, 255, .3);--cropper-background-color: var(--ion-color-dark);flex:1;height:100%;max-height:calc(100vh - 56px)}::ng-deep .ngx-ic-component{height:100%!important}::ng-deep .ngx-ic-source-image{max-height:100%!important}\n"] }]
|
|
21360
|
-
}], propDecorators: { cropComplete: [{
|
|
21361
|
-
type: Output
|
|
21362
|
-
}], cancel: [{
|
|
21363
|
-
type: Output
|
|
21364
|
-
}], loadFailed: [{
|
|
21365
|
-
type: Output
|
|
21366
|
-
}] } });
|
|
21367
|
-
|
|
21368
21199
|
/**
|
|
21369
21200
|
* Configuración de espaciado predefinida
|
|
21370
21201
|
*/
|
|
@@ -30754,15 +30585,6 @@ class AuthService {
|
|
|
30754
30585
|
.put(`${this.baseUrl}/profile`, request)
|
|
30755
30586
|
.pipe(catchError$1(error => this.handleAuthError(error)));
|
|
30756
30587
|
}
|
|
30757
|
-
/**
|
|
30758
|
-
* Actualiza el avatar del usuario en el backend.
|
|
30759
|
-
* Nota: El estado local del avatar se maneja a través de getProfile().
|
|
30760
|
-
*/
|
|
30761
|
-
updateAvatar(request) {
|
|
30762
|
-
return this.http
|
|
30763
|
-
.put(`${this.baseUrl}/profile/avatar`, request)
|
|
30764
|
-
.pipe(catchError$1(error => this.handleAuthError(error)));
|
|
30765
|
-
}
|
|
30766
30588
|
// =============================================
|
|
30767
30589
|
// RECUPERACIÓN DE CONTRASEÑA
|
|
30768
30590
|
// =============================================
|
|
@@ -30885,7 +30707,7 @@ class AuthService {
|
|
|
30885
30707
|
*/
|
|
30886
30708
|
updateHandle(handle) {
|
|
30887
30709
|
return this.http
|
|
30888
|
-
.put(`${this.baseUrl}/
|
|
30710
|
+
.put(`${this.baseUrl}/handle`, { handle })
|
|
30889
30711
|
.pipe(catchError$1(error => this.handleAuthError(error)));
|
|
30890
30712
|
}
|
|
30891
30713
|
/**
|
|
@@ -33679,607 +33501,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
33679
33501
|
type: Output
|
|
33680
33502
|
}] } });
|
|
33681
33503
|
|
|
33682
|
-
/**
|
|
33683
|
-
* Default values for image processing
|
|
33684
|
-
*/
|
|
33685
|
-
const IMAGE_DEFAULTS = {
|
|
33686
|
-
maxWidth: 800,
|
|
33687
|
-
maxHeight: 800,
|
|
33688
|
-
quality: 0.8,
|
|
33689
|
-
mimeType: 'image/jpeg',
|
|
33690
|
-
maxSize: 10 * 1024 * 1024, // 10MB
|
|
33691
|
-
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
|
|
33692
|
-
thumbnailSize: 150,
|
|
33693
|
-
};
|
|
33694
|
-
|
|
33695
|
-
/**
|
|
33696
|
-
* ImageService
|
|
33697
|
-
*
|
|
33698
|
-
* Service for image processing including compression, thumbnails, cropping and validation.
|
|
33699
|
-
* Uses HTML Canvas for all operations - no external dependencies.
|
|
33700
|
-
*
|
|
33701
|
-
* @example
|
|
33702
|
-
* ```typescript
|
|
33703
|
-
* const imageService = inject(ImageService);
|
|
33704
|
-
*
|
|
33705
|
-
* // Compress an image
|
|
33706
|
-
* const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
|
|
33707
|
-
*
|
|
33708
|
-
* // Generate thumbnail
|
|
33709
|
-
* const thumb = await imageService.thumbnail(file, 150);
|
|
33710
|
-
*
|
|
33711
|
-
* // Validate before processing
|
|
33712
|
-
* const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
|
|
33713
|
-
* if (!validation.valid) {
|
|
33714
|
-
* console.error(validation.message);
|
|
33715
|
-
* }
|
|
33716
|
-
* ```
|
|
33717
|
-
*/
|
|
33718
|
-
class ImageService {
|
|
33719
|
-
/**
|
|
33720
|
-
* Compress an image maintaining aspect ratio
|
|
33721
|
-
* @param file - File or Blob to compress
|
|
33722
|
-
* @param options - Compression options
|
|
33723
|
-
* @returns Promise with processed image data
|
|
33724
|
-
*/
|
|
33725
|
-
async compress(file, options) {
|
|
33726
|
-
const opts = {
|
|
33727
|
-
maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
|
|
33728
|
-
maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
|
|
33729
|
-
quality: options?.quality ?? IMAGE_DEFAULTS.quality,
|
|
33730
|
-
mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
|
|
33731
|
-
};
|
|
33732
|
-
const img = await this.loadImage(file);
|
|
33733
|
-
const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
|
|
33734
|
-
const canvas = document.createElement('canvas');
|
|
33735
|
-
canvas.width = width;
|
|
33736
|
-
canvas.height = height;
|
|
33737
|
-
const ctx = canvas.getContext('2d');
|
|
33738
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
33739
|
-
const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
|
|
33740
|
-
const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
|
|
33741
|
-
return {
|
|
33742
|
-
blob,
|
|
33743
|
-
dataUrl,
|
|
33744
|
-
width,
|
|
33745
|
-
height,
|
|
33746
|
-
size: blob.size,
|
|
33747
|
-
};
|
|
33748
|
-
}
|
|
33749
|
-
/**
|
|
33750
|
-
* Generate a square thumbnail from an image
|
|
33751
|
-
* @param file - File or Blob to process
|
|
33752
|
-
* @param size - Thumbnail size in pixels (default: 150)
|
|
33753
|
-
* @returns Promise with processed thumbnail
|
|
33754
|
-
*/
|
|
33755
|
-
async thumbnail(file, size) {
|
|
33756
|
-
const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
|
|
33757
|
-
const img = await this.loadImage(file);
|
|
33758
|
-
// Calculate square crop from center
|
|
33759
|
-
const minDim = Math.min(img.width, img.height);
|
|
33760
|
-
const cropX = (img.width - minDim) / 2;
|
|
33761
|
-
const cropY = (img.height - minDim) / 2;
|
|
33762
|
-
const canvas = document.createElement('canvas');
|
|
33763
|
-
canvas.width = thumbSize;
|
|
33764
|
-
canvas.height = thumbSize;
|
|
33765
|
-
const ctx = canvas.getContext('2d');
|
|
33766
|
-
ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
|
|
33767
|
-
const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
|
|
33768
|
-
);
|
|
33769
|
-
const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
|
|
33770
|
-
return {
|
|
33771
|
-
blob,
|
|
33772
|
-
dataUrl,
|
|
33773
|
-
width: thumbSize,
|
|
33774
|
-
height: thumbSize,
|
|
33775
|
-
size: blob.size,
|
|
33776
|
-
};
|
|
33777
|
-
}
|
|
33778
|
-
/**
|
|
33779
|
-
* Crop an image with specific coordinates
|
|
33780
|
-
* @param file - File or Blob to crop
|
|
33781
|
-
* @param cropData - Crop coordinates and dimensions
|
|
33782
|
-
* @param options - Optional compression options for output
|
|
33783
|
-
* @returns Promise with cropped image
|
|
33784
|
-
*/
|
|
33785
|
-
async crop(file, cropData, options) {
|
|
33786
|
-
const img = await this.loadImage(file);
|
|
33787
|
-
const opts = {
|
|
33788
|
-
quality: options?.quality ?? IMAGE_DEFAULTS.quality,
|
|
33789
|
-
mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
|
|
33790
|
-
};
|
|
33791
|
-
const canvas = document.createElement('canvas');
|
|
33792
|
-
canvas.width = cropData.width;
|
|
33793
|
-
canvas.height = cropData.height;
|
|
33794
|
-
const ctx = canvas.getContext('2d');
|
|
33795
|
-
ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
|
|
33796
|
-
// Apply max dimensions if specified
|
|
33797
|
-
if (options?.maxWidth || options?.maxHeight) {
|
|
33798
|
-
return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
|
|
33799
|
-
}
|
|
33800
|
-
const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
|
|
33801
|
-
const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
|
|
33802
|
-
return {
|
|
33803
|
-
blob,
|
|
33804
|
-
dataUrl,
|
|
33805
|
-
width: cropData.width,
|
|
33806
|
-
height: cropData.height,
|
|
33807
|
-
size: blob.size,
|
|
33808
|
-
};
|
|
33809
|
-
}
|
|
33810
|
-
/**
|
|
33811
|
-
* Validate an image file before processing
|
|
33812
|
-
* @param file - File to validate
|
|
33813
|
-
* @param options - Validation options
|
|
33814
|
-
* @returns Validation result with error details if invalid
|
|
33815
|
-
*/
|
|
33816
|
-
validate(file, options) {
|
|
33817
|
-
const opts = {
|
|
33818
|
-
maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
|
|
33819
|
-
allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
|
|
33820
|
-
};
|
|
33821
|
-
// Check file type
|
|
33822
|
-
if (!opts.allowedTypes.includes(file.type)) {
|
|
33823
|
-
return {
|
|
33824
|
-
valid: false,
|
|
33825
|
-
error: 'invalidType',
|
|
33826
|
-
message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
|
|
33827
|
-
};
|
|
33828
|
-
}
|
|
33829
|
-
// Check file size
|
|
33830
|
-
if (file.size > opts.maxSize) {
|
|
33831
|
-
const maxMB = Math.round(opts.maxSize / (1024 * 1024));
|
|
33832
|
-
return {
|
|
33833
|
-
valid: false,
|
|
33834
|
-
error: 'fileTooLarge',
|
|
33835
|
-
message: `La imagen es muy grande. Máximo ${maxMB}MB`,
|
|
33836
|
-
};
|
|
33837
|
-
}
|
|
33838
|
-
return { valid: true };
|
|
33839
|
-
}
|
|
33840
|
-
/**
|
|
33841
|
-
* Validate image dimensions (async - requires loading image)
|
|
33842
|
-
* @param file - File to validate
|
|
33843
|
-
* @param options - Validation options with minWidth/minHeight
|
|
33844
|
-
* @returns Promise with validation result
|
|
33845
|
-
*/
|
|
33846
|
-
async validateDimensions(file, options) {
|
|
33847
|
-
const img = await this.loadImage(file);
|
|
33848
|
-
if (options.minWidth && img.width < options.minWidth) {
|
|
33849
|
-
return {
|
|
33850
|
-
valid: false,
|
|
33851
|
-
error: 'imageTooSmall',
|
|
33852
|
-
message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
|
|
33853
|
-
};
|
|
33854
|
-
}
|
|
33855
|
-
if (options.minHeight && img.height < options.minHeight) {
|
|
33856
|
-
return {
|
|
33857
|
-
valid: false,
|
|
33858
|
-
error: 'imageTooSmall',
|
|
33859
|
-
message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
|
|
33860
|
-
};
|
|
33861
|
-
}
|
|
33862
|
-
return { valid: true };
|
|
33863
|
-
}
|
|
33864
|
-
/**
|
|
33865
|
-
* Convert a Blob/File to a data URL
|
|
33866
|
-
*/
|
|
33867
|
-
async toDataUrl(file) {
|
|
33868
|
-
return new Promise((resolve, reject) => {
|
|
33869
|
-
const reader = new FileReader();
|
|
33870
|
-
reader.onload = () => resolve(reader.result);
|
|
33871
|
-
reader.onerror = reject;
|
|
33872
|
-
reader.readAsDataURL(file);
|
|
33873
|
-
});
|
|
33874
|
-
}
|
|
33875
|
-
/**
|
|
33876
|
-
* Convert a data URL to a Blob
|
|
33877
|
-
*/
|
|
33878
|
-
dataUrlToBlob(dataUrl) {
|
|
33879
|
-
const arr = dataUrl.split(',');
|
|
33880
|
-
const mime = arr[0].match(/:(.*?);/)[1];
|
|
33881
|
-
const bstr = atob(arr[1]);
|
|
33882
|
-
let n = bstr.length;
|
|
33883
|
-
const u8arr = new Uint8Array(n);
|
|
33884
|
-
while (n--) {
|
|
33885
|
-
u8arr[n] = bstr.charCodeAt(n);
|
|
33886
|
-
}
|
|
33887
|
-
return new Blob([u8arr], { type: mime });
|
|
33888
|
-
}
|
|
33889
|
-
// ============== Private Helpers ==============
|
|
33890
|
-
loadImage(file) {
|
|
33891
|
-
return new Promise((resolve, reject) => {
|
|
33892
|
-
const img = new Image();
|
|
33893
|
-
img.onload = () => {
|
|
33894
|
-
URL.revokeObjectURL(img.src);
|
|
33895
|
-
resolve(img);
|
|
33896
|
-
};
|
|
33897
|
-
img.onerror = reject;
|
|
33898
|
-
img.src = URL.createObjectURL(file);
|
|
33899
|
-
});
|
|
33900
|
-
}
|
|
33901
|
-
calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
|
|
33902
|
-
let width = originalWidth;
|
|
33903
|
-
let height = originalHeight;
|
|
33904
|
-
// Scale down if necessary, maintaining aspect ratio
|
|
33905
|
-
if (width > maxWidth) {
|
|
33906
|
-
height = (height * maxWidth) / width;
|
|
33907
|
-
width = maxWidth;
|
|
33908
|
-
}
|
|
33909
|
-
if (height > maxHeight) {
|
|
33910
|
-
width = (width * maxHeight) / height;
|
|
33911
|
-
height = maxHeight;
|
|
33912
|
-
}
|
|
33913
|
-
return {
|
|
33914
|
-
width: Math.round(width),
|
|
33915
|
-
height: Math.round(height),
|
|
33916
|
-
};
|
|
33917
|
-
}
|
|
33918
|
-
canvasToBlob(canvas, mimeType, quality) {
|
|
33919
|
-
return new Promise((resolve, reject) => {
|
|
33920
|
-
canvas.toBlob((blob) => {
|
|
33921
|
-
if (blob)
|
|
33922
|
-
resolve(blob);
|
|
33923
|
-
else
|
|
33924
|
-
reject(new Error('Failed to create blob from canvas'));
|
|
33925
|
-
}, mimeType, quality);
|
|
33926
|
-
});
|
|
33927
|
-
}
|
|
33928
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
33929
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
|
|
33930
|
-
}
|
|
33931
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
|
|
33932
|
-
type: Injectable,
|
|
33933
|
-
args: [{ providedIn: 'root' }]
|
|
33934
|
-
}] });
|
|
33935
|
-
|
|
33936
|
-
/**
|
|
33937
|
-
* Default values
|
|
33938
|
-
*/
|
|
33939
|
-
const AVATAR_UPLOAD_DEFAULTS = {
|
|
33940
|
-
size: 100,
|
|
33941
|
-
editable: true,
|
|
33942
|
-
storagePath: 'avatars',
|
|
33943
|
-
i18nNamespace: 'AvatarUpload',
|
|
33944
|
-
maxFileSize: 10 * 1024 * 1024, // 10MB
|
|
33945
|
-
compressQuality: 0.8,
|
|
33946
|
-
maxWidth: 800,
|
|
33947
|
-
thumbnailSize: 150,
|
|
33948
|
-
backgroundColor: '#6366f1', // Indigo
|
|
33949
|
-
};
|
|
33950
|
-
|
|
33951
|
-
addIcons({ cameraOutline });
|
|
33952
|
-
/**
|
|
33953
|
-
* AvatarUploadComponent
|
|
33954
|
-
*
|
|
33955
|
-
* A complete avatar upload solution with:
|
|
33956
|
-
* - Image selection from device
|
|
33957
|
-
* - Crop modal with round preview
|
|
33958
|
-
* - Automatic compression and thumbnail generation
|
|
33959
|
-
* - Upload to Firebase Storage
|
|
33960
|
-
* - Backend sync via AuthService
|
|
33961
|
-
*
|
|
33962
|
-
* @example Basic usage
|
|
33963
|
-
* ```html
|
|
33964
|
-
* <val-avatar-upload
|
|
33965
|
-
* [props]="{
|
|
33966
|
-
* currentUrl: user()?.avatarUrl,
|
|
33967
|
-
* initials: 'JD',
|
|
33968
|
-
* size: 120
|
|
33969
|
-
* }"
|
|
33970
|
-
* (uploaded)="onAvatarUploaded($event)"
|
|
33971
|
-
* (error)="onError($event)"
|
|
33972
|
-
* />
|
|
33973
|
-
* ```
|
|
33974
|
-
*/
|
|
33975
|
-
class AvatarUploadComponent {
|
|
33976
|
-
constructor() {
|
|
33977
|
-
this.imageService = inject(ImageService);
|
|
33978
|
-
this.storageService = inject(StorageService);
|
|
33979
|
-
this.authService = inject(AuthService);
|
|
33980
|
-
this.i18n = inject(I18nService);
|
|
33981
|
-
/** Component configuration */
|
|
33982
|
-
this.props = input({});
|
|
33983
|
-
/** Emitted after successful upload and backend sync */
|
|
33984
|
-
this.uploaded = new EventEmitter();
|
|
33985
|
-
/** Emitted on any error during the process */
|
|
33986
|
-
this.error = new EventEmitter();
|
|
33987
|
-
/** Emitted when upload starts */
|
|
33988
|
-
this.uploadStart = new EventEmitter();
|
|
33989
|
-
// Internal state
|
|
33990
|
-
this.loading = signal(false);
|
|
33991
|
-
this.showCropModal = signal(false);
|
|
33992
|
-
this.selectedFile = signal(null);
|
|
33993
|
-
this.previewUrl = signal(null);
|
|
33994
|
-
this.imageLoadError = signal(false);
|
|
33995
|
-
/** Merged config with defaults */
|
|
33996
|
-
this.config = computed(() => ({
|
|
33997
|
-
...AVATAR_UPLOAD_DEFAULTS,
|
|
33998
|
-
...this.props(),
|
|
33999
|
-
}));
|
|
34000
|
-
/** URL to display (preview takes priority over current) */
|
|
34001
|
-
this.displayUrl = computed(() => {
|
|
34002
|
-
if (this.imageLoadError())
|
|
34003
|
-
return null;
|
|
34004
|
-
return this.previewUrl() || this.config().currentUrl || null;
|
|
34005
|
-
});
|
|
34006
|
-
/** Aria label for edit button */
|
|
34007
|
-
this.editButtonLabel = computed(() => {
|
|
34008
|
-
this.i18n.lang();
|
|
34009
|
-
return this.i18n.t('changePhoto', this.config().i18nNamespace) || 'Cambiar foto';
|
|
34010
|
-
});
|
|
34011
|
-
}
|
|
34012
|
-
/** Open file picker dialog */
|
|
34013
|
-
openFilePicker() {
|
|
34014
|
-
this.fileInput.nativeElement.click();
|
|
34015
|
-
}
|
|
34016
|
-
/** Handle file selection */
|
|
34017
|
-
onFileSelected(event) {
|
|
34018
|
-
const input = event.target;
|
|
34019
|
-
const file = input.files?.[0];
|
|
34020
|
-
if (!file)
|
|
34021
|
-
return;
|
|
34022
|
-
// Reset input for same file selection
|
|
34023
|
-
input.value = '';
|
|
34024
|
-
// Validate file
|
|
34025
|
-
const validation = this.imageService.validate(file, {
|
|
34026
|
-
maxSize: this.config().maxFileSize,
|
|
34027
|
-
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
34028
|
-
});
|
|
34029
|
-
if (!validation.valid) {
|
|
34030
|
-
this.emitError(validation.error, validation.message);
|
|
34031
|
-
return;
|
|
34032
|
-
}
|
|
34033
|
-
// Open crop modal
|
|
34034
|
-
this.selectedFile.set(file);
|
|
34035
|
-
this.showCropModal.set(true);
|
|
34036
|
-
}
|
|
34037
|
-
/** Handle crop completion */
|
|
34038
|
-
async onCropComplete(croppedBlob) {
|
|
34039
|
-
this.showCropModal.set(false);
|
|
34040
|
-
this.selectedFile.set(null);
|
|
34041
|
-
await this.processAndUpload(croppedBlob);
|
|
34042
|
-
}
|
|
34043
|
-
/** Handle crop cancel */
|
|
34044
|
-
onCropCancel() {
|
|
34045
|
-
this.showCropModal.set(false);
|
|
34046
|
-
this.selectedFile.set(null);
|
|
34047
|
-
}
|
|
34048
|
-
/** Handle crop load failure */
|
|
34049
|
-
onCropLoadFailed() {
|
|
34050
|
-
this.showCropModal.set(false);
|
|
34051
|
-
this.selectedFile.set(null);
|
|
34052
|
-
this.emitError('invalidType', this.i18n.t('loadFailed', this.config().i18nNamespace) || 'No se pudo cargar la imagen');
|
|
34053
|
-
}
|
|
34054
|
-
/** Handle image load error */
|
|
34055
|
-
onImageError() {
|
|
34056
|
-
this.imageLoadError.set(true);
|
|
34057
|
-
}
|
|
34058
|
-
/** Process cropped image and upload */
|
|
34059
|
-
async processAndUpload(croppedBlob) {
|
|
34060
|
-
this.loading.set(true);
|
|
34061
|
-
this.uploadStart.emit();
|
|
34062
|
-
try {
|
|
34063
|
-
const config = this.config();
|
|
34064
|
-
// 1. Compress image
|
|
34065
|
-
const compressed = await this.imageService.compress(croppedBlob, {
|
|
34066
|
-
maxWidth: config.maxWidth,
|
|
34067
|
-
maxHeight: config.maxWidth,
|
|
34068
|
-
quality: config.compressQuality,
|
|
34069
|
-
});
|
|
34070
|
-
// 2. Generate thumbnail
|
|
34071
|
-
const thumbnail = await this.imageService.thumbnail(compressed.blob, config.thumbnailSize);
|
|
34072
|
-
// 3. Set preview immediately
|
|
34073
|
-
this.previewUrl.set(compressed.dataUrl);
|
|
34074
|
-
this.imageLoadError.set(false);
|
|
34075
|
-
// 4. Get user ID for storage path
|
|
34076
|
-
const userId = this.authService.user()?.userId;
|
|
34077
|
-
if (!userId) {
|
|
34078
|
-
throw new Error('User not authenticated');
|
|
34079
|
-
}
|
|
34080
|
-
// 5. Upload to Firebase Storage
|
|
34081
|
-
const timestamp = Date.now();
|
|
34082
|
-
const avatarPath = `${config.storagePath}/${userId}/avatar_${timestamp}.jpg`;
|
|
34083
|
-
const thumbPath = `${config.storagePath}/${userId}/thumb_${timestamp}.jpg`;
|
|
34084
|
-
const [avatarResult, thumbResult] = await Promise.all([
|
|
34085
|
-
this.storageService.uploadAndGetUrl(avatarPath, compressed.blob),
|
|
34086
|
-
this.storageService.uploadAndGetUrl(thumbPath, thumbnail.blob),
|
|
34087
|
-
]);
|
|
34088
|
-
// 6. Update backend
|
|
34089
|
-
await firstValueFrom(this.authService.updateAvatar({
|
|
34090
|
-
avatarUrl: avatarResult.downloadUrl,
|
|
34091
|
-
avatarThumbnail: thumbResult.downloadUrl,
|
|
34092
|
-
}));
|
|
34093
|
-
// 7. Emit success
|
|
34094
|
-
const result = {
|
|
34095
|
-
avatarUrl: avatarResult.downloadUrl,
|
|
34096
|
-
thumbnailUrl: thumbResult.downloadUrl,
|
|
34097
|
-
};
|
|
34098
|
-
this.uploaded.emit(result);
|
|
34099
|
-
}
|
|
34100
|
-
catch (err) {
|
|
34101
|
-
// Revert preview on error
|
|
34102
|
-
this.previewUrl.set(null);
|
|
34103
|
-
const message = err instanceof Error
|
|
34104
|
-
? err.message
|
|
34105
|
-
: this.i18n.t('uploadError', this.config().i18nNamespace) || 'Error al subir la imagen';
|
|
34106
|
-
this.emitError('uploadFailed', message, err);
|
|
34107
|
-
}
|
|
34108
|
-
finally {
|
|
34109
|
-
this.loading.set(false);
|
|
34110
|
-
}
|
|
34111
|
-
}
|
|
34112
|
-
/** Emit error event */
|
|
34113
|
-
emitError(type, message, originalError) {
|
|
34114
|
-
this.error.emit({ type, message, originalError });
|
|
34115
|
-
}
|
|
34116
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AvatarUploadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
34117
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: AvatarUploadComponent, isStandalone: true, selector: "val-avatar-upload", inputs: { props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { uploaded: "uploaded", error: "error", uploadStart: "uploadStart" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }], ngImport: i0, template: `
|
|
34118
|
-
<div
|
|
34119
|
-
class="avatar-upload"
|
|
34120
|
-
[style.--avatar-size.px]="config().size"
|
|
34121
|
-
[class.avatar-upload--loading]="loading()"
|
|
34122
|
-
>
|
|
34123
|
-
<div class="avatar-container">
|
|
34124
|
-
<!-- Avatar Image or Initials -->
|
|
34125
|
-
@if (displayUrl()) {
|
|
34126
|
-
<img
|
|
34127
|
-
class="avatar-image"
|
|
34128
|
-
[src]="displayUrl()"
|
|
34129
|
-
alt="Avatar"
|
|
34130
|
-
(error)="onImageError()"
|
|
34131
|
-
/>
|
|
34132
|
-
} @else {
|
|
34133
|
-
<div
|
|
34134
|
-
class="avatar-initials"
|
|
34135
|
-
[style.background-color]="config().backgroundColor"
|
|
34136
|
-
>
|
|
34137
|
-
{{ config().initials || '?' }}
|
|
34138
|
-
</div>
|
|
34139
|
-
}
|
|
34140
|
-
|
|
34141
|
-
<!-- Edit Button -->
|
|
34142
|
-
@if (config().editable && !loading()) {
|
|
34143
|
-
<button
|
|
34144
|
-
class="edit-button"
|
|
34145
|
-
type="button"
|
|
34146
|
-
(click)="openFilePicker()"
|
|
34147
|
-
[attr.aria-label]="editButtonLabel()"
|
|
34148
|
-
>
|
|
34149
|
-
<ion-icon name="camera-outline"></ion-icon>
|
|
34150
|
-
</button>
|
|
34151
|
-
}
|
|
34152
|
-
|
|
34153
|
-
<!-- Loading Overlay -->
|
|
34154
|
-
@if (loading()) {
|
|
34155
|
-
<div class="loading-overlay">
|
|
34156
|
-
<ion-spinner name="crescent"></ion-spinner>
|
|
34157
|
-
</div>
|
|
34158
|
-
}
|
|
34159
|
-
</div>
|
|
34160
|
-
|
|
34161
|
-
<!-- Hidden File Input -->
|
|
34162
|
-
<input
|
|
34163
|
-
#fileInput
|
|
34164
|
-
type="file"
|
|
34165
|
-
accept="image/jpeg,image/png,image/webp"
|
|
34166
|
-
(change)="onFileSelected($event)"
|
|
34167
|
-
hidden
|
|
34168
|
-
/>
|
|
34169
|
-
|
|
34170
|
-
<!-- Crop Modal -->
|
|
34171
|
-
<ion-modal
|
|
34172
|
-
[isOpen]="showCropModal()"
|
|
34173
|
-
(didDismiss)="onCropCancel()"
|
|
34174
|
-
[breakpoints]="[0, 1]"
|
|
34175
|
-
[initialBreakpoint]="1"
|
|
34176
|
-
>
|
|
34177
|
-
<ng-template>
|
|
34178
|
-
@if (selectedFile()) {
|
|
34179
|
-
<val-image-crop
|
|
34180
|
-
[image]="selectedFile()!"
|
|
34181
|
-
[aspectRatio]="1"
|
|
34182
|
-
[roundCropper]="true"
|
|
34183
|
-
[i18nNamespace]="config().i18nNamespace"
|
|
34184
|
-
(cropComplete)="onCropComplete($event)"
|
|
34185
|
-
(cancel)="onCropCancel()"
|
|
34186
|
-
(loadFailed)="onCropLoadFailed()"
|
|
34187
|
-
/>
|
|
34188
|
-
}
|
|
34189
|
-
</ng-template>
|
|
34190
|
-
</ion-modal>
|
|
34191
|
-
</div>
|
|
34192
|
-
`, isInline: true, styles: [".avatar-upload{--avatar-size: 100px;--edit-button-size: 32px;--edit-button-offset: 4px;display:inline-block}.avatar-container{position:relative;width:var(--avatar-size);height:var(--avatar-size);border-radius:50%;overflow:visible}.avatar-image{width:100%;height:100%;border-radius:50%;object-fit:cover;background-color:var(--ion-color-light)}.avatar-initials{width:100%;height:100%;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:calc(var(--avatar-size) * .4);font-weight:600;color:#fff;text-transform:uppercase;-webkit-user-select:none;user-select:none}.edit-button{position:absolute;bottom:var(--edit-button-offset);right:var(--edit-button-offset);width:var(--edit-button-size);height:var(--edit-button-size);border-radius:50%;border:2px solid white;background:var(--ion-color-primary);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .2s ease,background-color .2s ease;box-shadow:0 2px 8px #00000026}.edit-button ion-icon{font-size:calc(var(--edit-button-size) * .5)}.edit-button:hover{transform:scale(1.1);background:var(--ion-color-primary-shade)}.edit-button:active{transform:scale(.95)}.edit-button:focus-visible{outline:2px solid var(--ion-color-primary);outline-offset:2px}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:50%;background:#00000080;display:flex;align-items:center;justify-content:center}.loading-overlay ion-spinner{--color: white;width:calc(var(--avatar-size) * .4);height:calc(var(--avatar-size) * .4)}.avatar-upload--loading .edit-button{display:none}.avatar-upload--loading .avatar-image,.avatar-upload--loading .avatar-initials{filter:brightness(.7)}@container (max-width: 60px){.edit-button{--edit-button-size: 24px;--edit-button-offset: 0}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: IonModal, selector: "ion-modal" }, { kind: "component", type: ImageCropComponent, selector: "val-image-crop", inputs: ["image", "aspectRatio", "roundCropper", "resizeToWidth", "i18nNamespace"], outputs: ["cropComplete", "cancel", "loadFailed"] }] }); }
|
|
34193
|
-
}
|
|
34194
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: AvatarUploadComponent, decorators: [{
|
|
34195
|
-
type: Component,
|
|
34196
|
-
args: [{ selector: 'val-avatar-upload', standalone: true, imports: [CommonModule, IonIcon, IonSpinner, IonModal, ImageCropComponent], template: `
|
|
34197
|
-
<div
|
|
34198
|
-
class="avatar-upload"
|
|
34199
|
-
[style.--avatar-size.px]="config().size"
|
|
34200
|
-
[class.avatar-upload--loading]="loading()"
|
|
34201
|
-
>
|
|
34202
|
-
<div class="avatar-container">
|
|
34203
|
-
<!-- Avatar Image or Initials -->
|
|
34204
|
-
@if (displayUrl()) {
|
|
34205
|
-
<img
|
|
34206
|
-
class="avatar-image"
|
|
34207
|
-
[src]="displayUrl()"
|
|
34208
|
-
alt="Avatar"
|
|
34209
|
-
(error)="onImageError()"
|
|
34210
|
-
/>
|
|
34211
|
-
} @else {
|
|
34212
|
-
<div
|
|
34213
|
-
class="avatar-initials"
|
|
34214
|
-
[style.background-color]="config().backgroundColor"
|
|
34215
|
-
>
|
|
34216
|
-
{{ config().initials || '?' }}
|
|
34217
|
-
</div>
|
|
34218
|
-
}
|
|
34219
|
-
|
|
34220
|
-
<!-- Edit Button -->
|
|
34221
|
-
@if (config().editable && !loading()) {
|
|
34222
|
-
<button
|
|
34223
|
-
class="edit-button"
|
|
34224
|
-
type="button"
|
|
34225
|
-
(click)="openFilePicker()"
|
|
34226
|
-
[attr.aria-label]="editButtonLabel()"
|
|
34227
|
-
>
|
|
34228
|
-
<ion-icon name="camera-outline"></ion-icon>
|
|
34229
|
-
</button>
|
|
34230
|
-
}
|
|
34231
|
-
|
|
34232
|
-
<!-- Loading Overlay -->
|
|
34233
|
-
@if (loading()) {
|
|
34234
|
-
<div class="loading-overlay">
|
|
34235
|
-
<ion-spinner name="crescent"></ion-spinner>
|
|
34236
|
-
</div>
|
|
34237
|
-
}
|
|
34238
|
-
</div>
|
|
34239
|
-
|
|
34240
|
-
<!-- Hidden File Input -->
|
|
34241
|
-
<input
|
|
34242
|
-
#fileInput
|
|
34243
|
-
type="file"
|
|
34244
|
-
accept="image/jpeg,image/png,image/webp"
|
|
34245
|
-
(change)="onFileSelected($event)"
|
|
34246
|
-
hidden
|
|
34247
|
-
/>
|
|
34248
|
-
|
|
34249
|
-
<!-- Crop Modal -->
|
|
34250
|
-
<ion-modal
|
|
34251
|
-
[isOpen]="showCropModal()"
|
|
34252
|
-
(didDismiss)="onCropCancel()"
|
|
34253
|
-
[breakpoints]="[0, 1]"
|
|
34254
|
-
[initialBreakpoint]="1"
|
|
34255
|
-
>
|
|
34256
|
-
<ng-template>
|
|
34257
|
-
@if (selectedFile()) {
|
|
34258
|
-
<val-image-crop
|
|
34259
|
-
[image]="selectedFile()!"
|
|
34260
|
-
[aspectRatio]="1"
|
|
34261
|
-
[roundCropper]="true"
|
|
34262
|
-
[i18nNamespace]="config().i18nNamespace"
|
|
34263
|
-
(cropComplete)="onCropComplete($event)"
|
|
34264
|
-
(cancel)="onCropCancel()"
|
|
34265
|
-
(loadFailed)="onCropLoadFailed()"
|
|
34266
|
-
/>
|
|
34267
|
-
}
|
|
34268
|
-
</ng-template>
|
|
34269
|
-
</ion-modal>
|
|
34270
|
-
</div>
|
|
34271
|
-
`, styles: [".avatar-upload{--avatar-size: 100px;--edit-button-size: 32px;--edit-button-offset: 4px;display:inline-block}.avatar-container{position:relative;width:var(--avatar-size);height:var(--avatar-size);border-radius:50%;overflow:visible}.avatar-image{width:100%;height:100%;border-radius:50%;object-fit:cover;background-color:var(--ion-color-light)}.avatar-initials{width:100%;height:100%;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:calc(var(--avatar-size) * .4);font-weight:600;color:#fff;text-transform:uppercase;-webkit-user-select:none;user-select:none}.edit-button{position:absolute;bottom:var(--edit-button-offset);right:var(--edit-button-offset);width:var(--edit-button-size);height:var(--edit-button-size);border-radius:50%;border:2px solid white;background:var(--ion-color-primary);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .2s ease,background-color .2s ease;box-shadow:0 2px 8px #00000026}.edit-button ion-icon{font-size:calc(var(--edit-button-size) * .5)}.edit-button:hover{transform:scale(1.1);background:var(--ion-color-primary-shade)}.edit-button:active{transform:scale(.95)}.edit-button:focus-visible{outline:2px solid var(--ion-color-primary);outline-offset:2px}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;border-radius:50%;background:#00000080;display:flex;align-items:center;justify-content:center}.loading-overlay ion-spinner{--color: white;width:calc(var(--avatar-size) * .4);height:calc(var(--avatar-size) * .4)}.avatar-upload--loading .edit-button{display:none}.avatar-upload--loading .avatar-image,.avatar-upload--loading .avatar-initials{filter:brightness(.7)}@container (max-width: 60px){.edit-button{--edit-button-size: 24px;--edit-button-offset: 0}}\n"] }]
|
|
34272
|
-
}], propDecorators: { fileInput: [{
|
|
34273
|
-
type: ViewChild,
|
|
34274
|
-
args: ['fileInput']
|
|
34275
|
-
}], uploaded: [{
|
|
34276
|
-
type: Output
|
|
34277
|
-
}], error: [{
|
|
34278
|
-
type: Output
|
|
34279
|
-
}], uploadStart: [{
|
|
34280
|
-
type: Output
|
|
34281
|
-
}] } });
|
|
34282
|
-
|
|
34283
33504
|
class LayoutComponent {
|
|
34284
33505
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: LayoutComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
34285
33506
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.14", type: LayoutComponent, isStandalone: true, selector: "val-layout", ngImport: i0, template: `
|
|
@@ -41888,5 +41109,5 @@ function buildFooterLinks(links, t) {
|
|
|
41888
41109
|
* Generated bundle index. Do not edit.
|
|
41889
41110
|
*/
|
|
41890
41111
|
|
|
41891
|
-
export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING,
|
|
41112
|
+
export { ACTION_CARD_DEFAULTS, AD_SIZE_MAP, API_TABLE_COLUMN_LABELS, ARTICLE_SPACING, AccordionComponent, ActionCardComponent, ActionHeaderComponent, ActionType, AdSlotComponent, AdsLoaderService, AdsService, AlertBoxComponent, AnalyticsErrorHandler, AnalyticsRouterTracker, AnalyticsService, AppConfigService, ArticleBuilder, ArticleComponent, AuthBackgroundComponent, AuthService, AuthStateService, AuthStorageService, AuthSyncService, AvatarComponent, BOTTOM_NAV_DEFAULTS, BannerComponent, BaseDefault, BottomNavComponent, BoxComponent, BreadcrumbComponent, ButtonComponent, ButtonGroupComponent, COMMON_COUNTRY_CODES, COMMON_CURRENCIES, CURRENCY_INFO, CardComponent, CardSection, CardType, CardsCarouselComponent, CheckInputComponent, ChipGroupComponent, ClearDefault, ClearDefaultBlock, ClearDefaultFull, ClearDefaultRound, ClearDefaultRoundBlock, ClearDefaultRoundFull, CodeDisplayComponent, CommandDisplayComponent, CommentComponent, CommentInputComponent, CommentSectionComponent, CompanyFooterComponent, ComponentStates, ConfirmationDialogService, ContentLoaderComponent, ContentReactionComponent, CountdownComponent, CurrencyInputComponent, DEFAULT_ADS_CONFIG, DEFAULT_APP_CONFIG_SERVICE_CONFIG, DEFAULT_AUTH_CONFIG, DEFAULT_BACK_HEADER, DEFAULT_CANCEL_BUTTON, DEFAULT_CONFIRM_BUTTON, DEFAULT_COUNTDOWN_LABELS, DEFAULT_COUNTDOWN_LABELS_EN, DEFAULT_EMPTY_STATE, DEFAULT_EMULATOR_CONFIG, DEFAULT_FEEDBACK_CONFIG, DEFAULT_FEEDBACK_TYPE_OPTIONS, DEFAULT_HOME_HEADER, DEFAULT_INFINITE_LIST_METADATA, DEFAULT_LEGEND_LABELS, DEFAULT_MODAL_CANCEL_BUTTON, DEFAULT_MODAL_CONFIRM_BUTTON, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PAYMENT_STATUS_COLORS, DEFAULT_PAYMENT_STATUS_LABELS, DEFAULT_PLATFORMS, DEFAULT_REFRESHER_METADATA, DEFAULT_SKELETON_CONFIG, DEFAULT_STATUS_COLORS, DEFAULT_STATUS_LABELS, DEFAULT_WINNER_LABELS, DataTableComponent, DateInputComponent, DateRangeInputComponent, DetailSkeletonComponent, DeviceService, DisplayComponent, DividerComponent, DocsApiTableComponent, DocsBreadcrumbComponent, DocsCalloutComponent, DocsCodeExampleComponent, DocsLayoutComponent, DocsNavLinksComponent, DocsNavigationService, DocsPageComponent, DocsSearchComponent, DocsSectionComponent, DocsShellComponent, DocsSidebarComponent, DocsTocComponent, DownloadService, EmailInputComponent, ExpandableTextComponent, FEATURES_LIST_DEFAULTS, FabComponent, FeaturesListComponent, FeedbackFormComponent, FeedbackService, FileInputComponent, FirebaseService, FirestoreCollectionFactory, FirestoreService, FooterComponent, FooterLinksComponent, FormComponent, FormFooterComponent, FormSkeletonComponent, FunHeaderComponent, GlowCardComponent, GridSkeletonComponent, HeaderComponent, HintComponent, HorizontalScrollComponent, HourInputComponent, HrefComponent, I18nService, INITIAL_AUTH_STATE, INITIAL_MFA_STATE, Icon, IconComponent, IconService, ImageComponent, InAppBrowserService, InfiniteListComponent, InfoComponent, InputI18nHelper, InputType, ItemListComponent, LANG_STORAGE_KEY$1 as LANG_STORAGE_KEY, LOGIN_DEFAULTS, LanguageSelectorComponent, LayeredCardComponent, LayoutComponent, LinkComponent, LinkProcessorService, LinkedProvidersComponent, LinksAccordionComponent, LinksCakeComponent, ListSkeletonComponent, LoadingDirective, LocalStorageService, LocaleService, LoginComponent, MODAL_SIZES, MOTION, MaintenancePageComponent, MenuComponent, MessagingService, MetaService, ModalService, MultiSelectSearchComponent, NavigationService, NoContentComponent, NotesBoxComponent, NotificationsService, NumberFromToComponent, NumberInputComponent, NumberStepperComponent, OAUTH_PROVIDERS_INFO, OAuthCallbackComponent, OAuthService, OutlineDefault, OutlineDefaultBlock, OutlineDefaultFull, OutlineDefaultRound, OutlineDefaultRoundBlock, OutlineDefaultRoundFull, PLATFORM_CONFIGS, PageContentComponent, PageTemplateComponent, PageWrapperComponent, PaginationComponent, PaginationService, ParticipantCardComponent, PasswordInputComponent, PhoneInputComponent, PillComponent, PinInputComponent, PlainCodeBoxComponent, PopoverSelectorComponent, PresetService, PriceTagComponent, PrimarySolidBlockButton, PrimarySolidBlockHrefButton, PrimarySolidBlockIconButton, PrimarySolidBlockIconHrefButton, PrimarySolidDefaultRoundButton, PrimarySolidDefaultRoundHrefButton, PrimarySolidDefaultRoundIconButton, PrimarySolidDefaultRoundIconHrefButton, PrimarySolidFullButton, PrimarySolidFullHrefButton, PrimarySolidFullIconButton, PrimarySolidFullIconHrefButton, PrimarySolidLargeRoundButton, PrimarySolidLargeRoundHrefButton, PrimarySolidLargeRoundIconButton, PrimarySolidLargeRoundIconHrefButton, PrimarySolidSmallRoundButton, PrimarySolidSmallRoundHrefButton, PrimarySolidSmallRoundIconButton, PrimarySolidSmallRoundIconHrefButton, ProcessLinksPipe, ProfileSkeletonComponent, ProgressBarComponent, ProgressRingComponent, ProgressStatusComponent, PrompterComponent, QR_PRESETS, QrCodeComponent, QrGeneratorService, QueryBuilder, QuoteBoxComponent, RadioInputComponent, RaffleStatusCardComponent, RangeInputComponent, RatingComponent, RecapCardComponent, RefresherComponent, RightsFooterComponent, RotatingTextComponent, SKELETON_PRESETS, SearchSelectorComponent, SearchbarComponent, SecondarySolidBlockButton, SecondarySolidBlockHrefButton, SecondarySolidBlockIconButton, SecondarySolidBlockIconHrefButton, SecondarySolidDefaultRoundButton, SecondarySolidDefaultRoundHrefButton, SecondarySolidDefaultRoundIconButton, SecondarySolidDefaultRoundIconHrefButton, SecondarySolidFullButton, SecondarySolidFullHrefButton, SecondarySolidFullIconButton, SecondarySolidFullIconHrefButton, SecondarySolidLargeRoundButton, SecondarySolidLargeRoundHrefButton, SecondarySolidLargeRoundIconButton, SecondarySolidLargeRoundIconHrefButton, SecondarySolidSmallRoundButton, SecondarySolidSmallRoundHrefButton, SecondarySolidSmallRoundIconButton, SecondarySolidSmallRoundIconHrefButton, SegmentControlComponent, SelectSearchComponent, SessionService, ShareButtonsComponent, SimpleComponent, SkeletonComponent, SkeletonService, SolidBlockButton, SolidDefault, SolidDefaultBlock, SolidDefaultButton, SolidDefaultFull, SolidDefaultRound, SolidDefaultRoundBlock, SolidDefaultRoundButton, SolidDefaultRoundFull, SolidFullButton, SolidLargeButton, SolidLargeRoundButton, SolidSmallButton, SolidSmallRoundButton, StatsCardComponent, StepperComponent, StorageService, SwipeCarouselComponent, TabbedContentComponent, TableSkeletonComponent, TabsComponent, Terminal404Component, TestimonialCardComponent, TestimonialCarouselComponent, TextComponent, TextInputComponent, TextareaInputComponent, ThemeOption, ThemeService, TicketGridComponent, TimelineComponent, TitleBlockComponent, TitleComponent, ToastService, ToggleInputComponent, TokenService, ToolbarActionType, ToolbarComponent, TranslatePipe, TypedCollection, UpdateBannerComponent, UsernameInputComponent, VALTECH_ADS_CONFIG, VALTECH_APP_CONFIG, VALTECH_AUTH_CONFIG, VALTECH_COMPANY_LINKS, VALTECH_DEFAULT_CONTENT, VALTECH_FEEDBACK_CONFIG, VALTECH_FIREBASE_CONFIG, VALTECH_FOOTER_I18N, VALTECH_FOOTER_LOGO, VALTECH_LANGUAGE_SELECTOR, VALTECH_SOCIAL_LINKS, VERSION, WinnerDisplayComponent, WizardComponent, WizardFooterComponent, applyDefaultValueToControl, authGuard, authInterceptor, buildFooterLinks, buildPath, collections, createFirebaseConfig, createGlowCardProps, createInitialPaginationState, createNumberFromToField, createTitleProps, extractPathParams, getAppInfo, getAppVersion, getCollectionPath, getDocumentId, goToTop, guestGuard, hasEmulators, isAtEnd, isCollectionPath, isDocumentPath, isEmulatorMode, isValidPath, joinPath, maxLength, permissionGuard, permissionGuardFromRoute, provideValtechAds, provideValtechAppConfig, provideValtechAuth, provideValtechAuthInterceptor, provideValtechFeedback, provideValtechFirebase, provideValtechI18n, provideValtechPresets, provideValtechSkeleton, query, replaceSpecialChars, resolveColor, resolveInputDefaultValue, roleGuard, storagePaths, superAdminGuard };
|
|
41892
41113
|
//# sourceMappingURL=valtech-components.mjs.map
|