valtech-components 4.0.1038 → 4.0.1040
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/components/molecules/select-input/select-input.component.mjs +5 -3
- package/esm2022/lib/services/auth/auth.service.mjs +6 -2
- package/esm2022/lib/services/auth/types.mjs +1 -1
- package/esm2022/lib/services/firebase/storage.service.mjs +49 -1
- package/esm2022/lib/services/firebase/types.mjs +1 -1
- package/esm2022/lib/services/i18n/i18n.service.mjs +10 -5
- package/esm2022/lib/services/preferences/preferences.service.mjs +25 -4
- package/esm2022/lib/version.mjs +2 -2
- package/fesm2022/valtech-components.mjs +344 -265
- package/fesm2022/valtech-components.mjs.map +1 -1
- package/lib/components/molecules/phone-display/phone-display.component.d.ts +1 -1
- package/lib/services/auth/types.d.ts +2 -0
- package/lib/services/firebase/storage.service.d.ts +11 -1
- package/lib/services/firebase/types.d.ts +34 -0
- package/lib/services/preferences/preferences.service.d.ts +2 -0
- package/lib/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -70,7 +70,7 @@ import fixWebmDuration from 'fix-webm-duration';
|
|
|
70
70
|
* Current version of valtech-components.
|
|
71
71
|
* This is automatically updated during the publish process.
|
|
72
72
|
*/
|
|
73
|
-
const VERSION = '4.0.
|
|
73
|
+
const VERSION = '4.0.1040';
|
|
74
74
|
|
|
75
75
|
function evaluateValtechAccess(rule, context, features = {}, visitedFeatures = new Set()) {
|
|
76
76
|
if (rule == null)
|
|
@@ -1876,13 +1876,18 @@ class I18nService {
|
|
|
1876
1876
|
console.warn(`[i18n] Language '${lang}' not in supported languages`);
|
|
1877
1877
|
return;
|
|
1878
1878
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
// Persistir en localStorage (browser-only)
|
|
1879
|
+
// Aunque el signal ya tenga este valor, re-persistimos. Esto corrige
|
|
1880
|
+
// estados viejos donde la UI quedó en el idioma correcto por snapshot, pero
|
|
1881
|
+
// localStorage conservó otro valor y el siguiente bootstrap volvió al viejo.
|
|
1883
1882
|
if (this.isBrowser) {
|
|
1884
1883
|
localStorage.setItem(LANG_STORAGE_KEY$1, lang);
|
|
1885
1884
|
}
|
|
1885
|
+
if (lang === this._lang()) {
|
|
1886
|
+
if (forceReload && this.isBrowser) {
|
|
1887
|
+
window.location.reload();
|
|
1888
|
+
}
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1886
1891
|
// Actualizar signal
|
|
1887
1892
|
this._lang.set(lang);
|
|
1888
1893
|
// Fallback: recargar si se solicita
|
|
@@ -5366,6 +5371,260 @@ function query() {
|
|
|
5366
5371
|
return new QueryBuilder();
|
|
5367
5372
|
}
|
|
5368
5373
|
|
|
5374
|
+
/**
|
|
5375
|
+
* Default values for image processing
|
|
5376
|
+
*/
|
|
5377
|
+
const IMAGE_DEFAULTS = {
|
|
5378
|
+
maxWidth: 800,
|
|
5379
|
+
maxHeight: 800,
|
|
5380
|
+
quality: 0.8,
|
|
5381
|
+
mimeType: 'image/jpeg',
|
|
5382
|
+
maxSize: 10 * 1024 * 1024, // 10MB
|
|
5383
|
+
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
|
|
5384
|
+
thumbnailSize: 150,
|
|
5385
|
+
};
|
|
5386
|
+
|
|
5387
|
+
/**
|
|
5388
|
+
* ImageService
|
|
5389
|
+
*
|
|
5390
|
+
* Service for image processing including compression, thumbnails, cropping and validation.
|
|
5391
|
+
* Uses HTML Canvas for all operations - no external dependencies.
|
|
5392
|
+
*
|
|
5393
|
+
* @example
|
|
5394
|
+
* ```typescript
|
|
5395
|
+
* const imageService = inject(ImageService);
|
|
5396
|
+
*
|
|
5397
|
+
* // Compress an image
|
|
5398
|
+
* const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
|
|
5399
|
+
*
|
|
5400
|
+
* // Generate thumbnail
|
|
5401
|
+
* const thumb = await imageService.thumbnail(file, 150);
|
|
5402
|
+
*
|
|
5403
|
+
* // Validate before processing
|
|
5404
|
+
* const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
|
|
5405
|
+
* if (!validation.valid) {
|
|
5406
|
+
* console.error(validation.message);
|
|
5407
|
+
* }
|
|
5408
|
+
* ```
|
|
5409
|
+
*/
|
|
5410
|
+
class ImageService {
|
|
5411
|
+
/**
|
|
5412
|
+
* Compress an image maintaining aspect ratio
|
|
5413
|
+
* @param file - File or Blob to compress
|
|
5414
|
+
* @param options - Compression options
|
|
5415
|
+
* @returns Promise with processed image data
|
|
5416
|
+
*/
|
|
5417
|
+
async compress(file, options) {
|
|
5418
|
+
const opts = {
|
|
5419
|
+
maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
|
|
5420
|
+
maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
|
|
5421
|
+
quality: options?.quality ?? IMAGE_DEFAULTS.quality,
|
|
5422
|
+
mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
|
|
5423
|
+
};
|
|
5424
|
+
const img = await this.loadImage(file);
|
|
5425
|
+
const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
|
|
5426
|
+
const canvas = document.createElement('canvas');
|
|
5427
|
+
canvas.width = width;
|
|
5428
|
+
canvas.height = height;
|
|
5429
|
+
const ctx = canvas.getContext('2d');
|
|
5430
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
5431
|
+
const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
|
|
5432
|
+
const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
|
|
5433
|
+
return {
|
|
5434
|
+
blob,
|
|
5435
|
+
dataUrl,
|
|
5436
|
+
width,
|
|
5437
|
+
height,
|
|
5438
|
+
size: blob.size,
|
|
5439
|
+
};
|
|
5440
|
+
}
|
|
5441
|
+
/**
|
|
5442
|
+
* Generate a square thumbnail from an image
|
|
5443
|
+
* @param file - File or Blob to process
|
|
5444
|
+
* @param size - Thumbnail size in pixels (default: 150)
|
|
5445
|
+
* @returns Promise with processed thumbnail
|
|
5446
|
+
*/
|
|
5447
|
+
async thumbnail(file, size) {
|
|
5448
|
+
const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
|
|
5449
|
+
const img = await this.loadImage(file);
|
|
5450
|
+
// Calculate square crop from center
|
|
5451
|
+
const minDim = Math.min(img.width, img.height);
|
|
5452
|
+
const cropX = (img.width - minDim) / 2;
|
|
5453
|
+
const cropY = (img.height - minDim) / 2;
|
|
5454
|
+
const canvas = document.createElement('canvas');
|
|
5455
|
+
canvas.width = thumbSize;
|
|
5456
|
+
canvas.height = thumbSize;
|
|
5457
|
+
const ctx = canvas.getContext('2d');
|
|
5458
|
+
ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
|
|
5459
|
+
const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
|
|
5460
|
+
);
|
|
5461
|
+
const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
|
|
5462
|
+
return {
|
|
5463
|
+
blob,
|
|
5464
|
+
dataUrl,
|
|
5465
|
+
width: thumbSize,
|
|
5466
|
+
height: thumbSize,
|
|
5467
|
+
size: blob.size,
|
|
5468
|
+
};
|
|
5469
|
+
}
|
|
5470
|
+
/**
|
|
5471
|
+
* Crop an image with specific coordinates
|
|
5472
|
+
* @param file - File or Blob to crop
|
|
5473
|
+
* @param cropData - Crop coordinates and dimensions
|
|
5474
|
+
* @param options - Optional compression options for output
|
|
5475
|
+
* @returns Promise with cropped image
|
|
5476
|
+
*/
|
|
5477
|
+
async crop(file, cropData, options) {
|
|
5478
|
+
const img = await this.loadImage(file);
|
|
5479
|
+
const opts = {
|
|
5480
|
+
quality: options?.quality ?? IMAGE_DEFAULTS.quality,
|
|
5481
|
+
mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
|
|
5482
|
+
};
|
|
5483
|
+
const canvas = document.createElement('canvas');
|
|
5484
|
+
canvas.width = cropData.width;
|
|
5485
|
+
canvas.height = cropData.height;
|
|
5486
|
+
const ctx = canvas.getContext('2d');
|
|
5487
|
+
ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
|
|
5488
|
+
// Apply max dimensions if specified
|
|
5489
|
+
if (options?.maxWidth || options?.maxHeight) {
|
|
5490
|
+
return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
|
|
5491
|
+
}
|
|
5492
|
+
const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
|
|
5493
|
+
const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
|
|
5494
|
+
return {
|
|
5495
|
+
blob,
|
|
5496
|
+
dataUrl,
|
|
5497
|
+
width: cropData.width,
|
|
5498
|
+
height: cropData.height,
|
|
5499
|
+
size: blob.size,
|
|
5500
|
+
};
|
|
5501
|
+
}
|
|
5502
|
+
/**
|
|
5503
|
+
* Validate an image file before processing
|
|
5504
|
+
* @param file - File to validate
|
|
5505
|
+
* @param options - Validation options
|
|
5506
|
+
* @returns Validation result with error details if invalid
|
|
5507
|
+
*/
|
|
5508
|
+
validate(file, options) {
|
|
5509
|
+
const opts = {
|
|
5510
|
+
maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
|
|
5511
|
+
allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
|
|
5512
|
+
};
|
|
5513
|
+
// Check file type
|
|
5514
|
+
if (!opts.allowedTypes.includes(file.type)) {
|
|
5515
|
+
return {
|
|
5516
|
+
valid: false,
|
|
5517
|
+
error: 'invalidType',
|
|
5518
|
+
message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
|
|
5519
|
+
};
|
|
5520
|
+
}
|
|
5521
|
+
// Check file size
|
|
5522
|
+
if (file.size > opts.maxSize) {
|
|
5523
|
+
const maxMB = Math.round(opts.maxSize / (1024 * 1024));
|
|
5524
|
+
return {
|
|
5525
|
+
valid: false,
|
|
5526
|
+
error: 'fileTooLarge',
|
|
5527
|
+
message: `La imagen es muy grande. Máximo ${maxMB}MB`,
|
|
5528
|
+
};
|
|
5529
|
+
}
|
|
5530
|
+
return { valid: true };
|
|
5531
|
+
}
|
|
5532
|
+
/**
|
|
5533
|
+
* Validate image dimensions (async - requires loading image)
|
|
5534
|
+
* @param file - File to validate
|
|
5535
|
+
* @param options - Validation options with minWidth/minHeight
|
|
5536
|
+
* @returns Promise with validation result
|
|
5537
|
+
*/
|
|
5538
|
+
async validateDimensions(file, options) {
|
|
5539
|
+
const img = await this.loadImage(file);
|
|
5540
|
+
if (options.minWidth && img.width < options.minWidth) {
|
|
5541
|
+
return {
|
|
5542
|
+
valid: false,
|
|
5543
|
+
error: 'imageTooSmall',
|
|
5544
|
+
message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
|
|
5545
|
+
};
|
|
5546
|
+
}
|
|
5547
|
+
if (options.minHeight && img.height < options.minHeight) {
|
|
5548
|
+
return {
|
|
5549
|
+
valid: false,
|
|
5550
|
+
error: 'imageTooSmall',
|
|
5551
|
+
message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
|
|
5552
|
+
};
|
|
5553
|
+
}
|
|
5554
|
+
return { valid: true };
|
|
5555
|
+
}
|
|
5556
|
+
/**
|
|
5557
|
+
* Convert a Blob/File to a data URL
|
|
5558
|
+
*/
|
|
5559
|
+
async toDataUrl(file) {
|
|
5560
|
+
return new Promise((resolve, reject) => {
|
|
5561
|
+
const reader = new FileReader();
|
|
5562
|
+
reader.onload = () => resolve(reader.result);
|
|
5563
|
+
reader.onerror = reject;
|
|
5564
|
+
reader.readAsDataURL(file);
|
|
5565
|
+
});
|
|
5566
|
+
}
|
|
5567
|
+
/**
|
|
5568
|
+
* Convert a data URL to a Blob
|
|
5569
|
+
*/
|
|
5570
|
+
dataUrlToBlob(dataUrl) {
|
|
5571
|
+
const arr = dataUrl.split(',');
|
|
5572
|
+
const mime = arr[0].match(/:(.*?);/)[1];
|
|
5573
|
+
const bstr = atob(arr[1]);
|
|
5574
|
+
let n = bstr.length;
|
|
5575
|
+
const u8arr = new Uint8Array(n);
|
|
5576
|
+
while (n--) {
|
|
5577
|
+
u8arr[n] = bstr.charCodeAt(n);
|
|
5578
|
+
}
|
|
5579
|
+
return new Blob([u8arr], { type: mime });
|
|
5580
|
+
}
|
|
5581
|
+
// ============== Private Helpers ==============
|
|
5582
|
+
loadImage(file) {
|
|
5583
|
+
return new Promise((resolve, reject) => {
|
|
5584
|
+
const img = new Image();
|
|
5585
|
+
img.onload = () => {
|
|
5586
|
+
URL.revokeObjectURL(img.src);
|
|
5587
|
+
resolve(img);
|
|
5588
|
+
};
|
|
5589
|
+
img.onerror = reject;
|
|
5590
|
+
img.src = URL.createObjectURL(file);
|
|
5591
|
+
});
|
|
5592
|
+
}
|
|
5593
|
+
calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
|
|
5594
|
+
let width = originalWidth;
|
|
5595
|
+
let height = originalHeight;
|
|
5596
|
+
// Scale down if necessary, maintaining aspect ratio
|
|
5597
|
+
if (width > maxWidth) {
|
|
5598
|
+
height = (height * maxWidth) / width;
|
|
5599
|
+
width = maxWidth;
|
|
5600
|
+
}
|
|
5601
|
+
if (height > maxHeight) {
|
|
5602
|
+
width = (width * maxHeight) / height;
|
|
5603
|
+
height = maxHeight;
|
|
5604
|
+
}
|
|
5605
|
+
return {
|
|
5606
|
+
width: Math.round(width),
|
|
5607
|
+
height: Math.round(height),
|
|
5608
|
+
};
|
|
5609
|
+
}
|
|
5610
|
+
canvasToBlob(canvas, mimeType, quality) {
|
|
5611
|
+
return new Promise((resolve, reject) => {
|
|
5612
|
+
canvas.toBlob((blob) => {
|
|
5613
|
+
if (blob)
|
|
5614
|
+
resolve(blob);
|
|
5615
|
+
else
|
|
5616
|
+
reject(new Error('Failed to create blob from canvas'));
|
|
5617
|
+
}, mimeType, quality);
|
|
5618
|
+
});
|
|
5619
|
+
}
|
|
5620
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
5621
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
|
|
5622
|
+
}
|
|
5623
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
|
|
5624
|
+
type: Injectable,
|
|
5625
|
+
args: [{ providedIn: 'root' }]
|
|
5626
|
+
}] });
|
|
5627
|
+
|
|
5369
5628
|
/**
|
|
5370
5629
|
* Storage Service
|
|
5371
5630
|
*
|
|
@@ -5404,6 +5663,7 @@ class StorageService {
|
|
|
5404
5663
|
constructor(storage) {
|
|
5405
5664
|
this.storage = storage;
|
|
5406
5665
|
this.config = inject(VALTECH_FIREBASE_CONFIG, { optional: true });
|
|
5666
|
+
this.imageService = inject(ImageService);
|
|
5407
5667
|
}
|
|
5408
5668
|
/**
|
|
5409
5669
|
* Prefija el path de storage con el appId si está configurado.
|
|
@@ -5649,6 +5909,52 @@ class StorageService {
|
|
|
5649
5909
|
}
|
|
5650
5910
|
return sdkResult;
|
|
5651
5911
|
}
|
|
5912
|
+
/**
|
|
5913
|
+
* Procesa y sube una imagen.
|
|
5914
|
+
*
|
|
5915
|
+
* A diferencia de `uploadAndGetUrl`, este método es específico para imágenes:
|
|
5916
|
+
* comprime por defecto con `ImageService` y puede generar un thumbnail en un
|
|
5917
|
+
* path separado. Para archivos que deben conservarse byte a byte, usar
|
|
5918
|
+
* `uploadAndGetUrl`.
|
|
5919
|
+
*/
|
|
5920
|
+
async uploadImageAndGetUrl(path, file, options) {
|
|
5921
|
+
const { compress = true, compression, thumbnailPath, thumbnailSize, thumbnailMetadata, ...metadata } = options ?? {};
|
|
5922
|
+
const processed = compress
|
|
5923
|
+
? await this.imageService.compress(file, compression)
|
|
5924
|
+
: {
|
|
5925
|
+
blob: await this.materialize(file),
|
|
5926
|
+
width: 0,
|
|
5927
|
+
height: 0,
|
|
5928
|
+
size: file.size,
|
|
5929
|
+
};
|
|
5930
|
+
const imageContentType = compress
|
|
5931
|
+
? (compression?.mimeType ?? IMAGE_DEFAULTS.mimeType)
|
|
5932
|
+
: (metadata.contentType ?? file.type ?? 'application/octet-stream');
|
|
5933
|
+
const result = await this.uploadAndGetUrl(path, processed.blob, {
|
|
5934
|
+
...metadata,
|
|
5935
|
+
contentType: imageContentType,
|
|
5936
|
+
});
|
|
5937
|
+
let thumbnail;
|
|
5938
|
+
if (thumbnailPath) {
|
|
5939
|
+
const thumb = await this.imageService.thumbnail(processed.blob, thumbnailSize);
|
|
5940
|
+
thumbnail = await this.uploadAndGetUrl(thumbnailPath, thumb.blob, {
|
|
5941
|
+
...metadata,
|
|
5942
|
+
...thumbnailMetadata,
|
|
5943
|
+
contentType: thumbnailMetadata?.contentType ?? IMAGE_DEFAULTS.mimeType,
|
|
5944
|
+
});
|
|
5945
|
+
}
|
|
5946
|
+
return {
|
|
5947
|
+
...result,
|
|
5948
|
+
thumbnail,
|
|
5949
|
+
processed: {
|
|
5950
|
+
originalSize: file.size,
|
|
5951
|
+
size: processed.blob.size,
|
|
5952
|
+
width: processed.width,
|
|
5953
|
+
height: processed.height,
|
|
5954
|
+
compressed: compress,
|
|
5955
|
+
},
|
|
5956
|
+
};
|
|
5957
|
+
}
|
|
5652
5958
|
/**
|
|
5653
5959
|
* Sube un archivo desde una Data URL (base64).
|
|
5654
5960
|
*
|
|
@@ -10651,7 +10957,11 @@ class AuthService {
|
|
|
10651
10957
|
handle: profile.handle ?? null,
|
|
10652
10958
|
avatarUrl: profile.avatarUrl ?? null,
|
|
10653
10959
|
phone: profile.phone ?? null,
|
|
10654
|
-
})),
|
|
10960
|
+
})), tap(profile => {
|
|
10961
|
+
if (this.i18nService && profile.language) {
|
|
10962
|
+
this.i18nService.setLanguage(profile.language);
|
|
10963
|
+
}
|
|
10964
|
+
}), catchError(error => this.handleAuthError(error)));
|
|
10655
10965
|
}
|
|
10656
10966
|
/**
|
|
10657
10967
|
* Actualiza el perfil del usuario. Sincroniza state con los nuevos values
|
|
@@ -22757,6 +23067,7 @@ class SearchSelectorComponent {
|
|
|
22757
23067
|
[interfaceOptions]="customPopoverOptions"
|
|
22758
23068
|
[interface]="resolvedProps.selectInterface || 'popover'"
|
|
22759
23069
|
[placeholder]="resolvedProps.placeholder"
|
|
23070
|
+
toggleIcon="chevron-down-outline"
|
|
22760
23071
|
[cancelText]="cancelText"
|
|
22761
23072
|
[okText]="okText"
|
|
22762
23073
|
>
|
|
@@ -22782,7 +23093,7 @@ class SearchSelectorComponent {
|
|
|
22782
23093
|
</ion-select-option>
|
|
22783
23094
|
}
|
|
22784
23095
|
</ion-select>
|
|
22785
|
-
`, isInline: true, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-xl-wide: 1280px;--val-container-md-wide: 900px;--val-container-aside: 0px;--val-container-padding: 16px;--val-radius-xs: 10px;--val-radius-sm: 16px;--val-radius-md: 20px;--val-radius-lg: 28px;--val-radius-xl: 36px;--val-radius-full: 999px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #737478;--ion-color-medium-rgb: 115,116,120;--ion-color-medium-contrast: #ffffff;--ion-color-medium-contrast-rgb: 255,255,255;--ion-color-medium-shade: #65666a;--ion-color-medium-tint: #818286;--ion-color-warning: #ffde38;--ion-color-warning-rgb: 255, 222, 56;--ion-color-warning-contrast: #000000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0c331;--ion-color-warning-tint: #ffe14c;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-dark)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}ion-select{--padding-top: 0;--padding-bottom: 0;--padding-start: 0;--padding-end: 0;min-height:auto;border:.0625rem solid var(--ion-color-medium);border-radius:1.5rem;margin-top:.375rem;padding-top:.5rem!important;padding-bottom:.5rem!important;padding-inline-start:1rem!important;padding-inline-end:.
|
|
23096
|
+
`, isInline: true, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-xl-wide: 1280px;--val-container-md-wide: 900px;--val-container-aside: 0px;--val-container-padding: 16px;--val-radius-xs: 10px;--val-radius-sm: 16px;--val-radius-md: 20px;--val-radius-lg: 28px;--val-radius-xl: 36px;--val-radius-full: 999px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #737478;--ion-color-medium-rgb: 115,116,120;--ion-color-medium-contrast: #ffffff;--ion-color-medium-contrast-rgb: 255,255,255;--ion-color-medium-shade: #65666a;--ion-color-medium-tint: #818286;--ion-color-warning: #ffde38;--ion-color-warning-rgb: 255, 222, 56;--ion-color-warning-contrast: #000000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0c331;--ion-color-warning-tint: #ffe14c;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-dark)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}ion-select{--padding-top: 0;--padding-bottom: 0;--padding-start: 0;--padding-end: 0;min-height:auto;border:.0625rem solid var(--ion-color-medium);border-radius:1.5rem;margin-top:.375rem;padding-top:.5rem!important;padding-bottom:.5rem!important;padding-inline-start:1rem!important;padding-inline-end:.875rem!important}.select-option-content{display:inline-flex;align-items:center;gap:.625rem}.select-option-icon{flex:0 0 auto;font-size:1.125rem}.select-option-icon-mask{width:1.125rem;height:1.125rem;background:currentColor;mask:var(--val-select-option-icon-src) center/contain no-repeat;-webkit-mask:var(--val-select-option-icon-src) center/contain no-repeat}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$8.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$8.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }] }); }
|
|
22786
23097
|
}
|
|
22787
23098
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SearchSelectorComponent, decorators: [{
|
|
22788
23099
|
type: Component,
|
|
@@ -22793,6 +23104,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
22793
23104
|
[interfaceOptions]="customPopoverOptions"
|
|
22794
23105
|
[interface]="resolvedProps.selectInterface || 'popover'"
|
|
22795
23106
|
[placeholder]="resolvedProps.placeholder"
|
|
23107
|
+
toggleIcon="chevron-down-outline"
|
|
22796
23108
|
[cancelText]="cancelText"
|
|
22797
23109
|
[okText]="okText"
|
|
22798
23110
|
>
|
|
@@ -22818,7 +23130,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
22818
23130
|
</ion-select-option>
|
|
22819
23131
|
}
|
|
22820
23132
|
</ion-select>
|
|
22821
|
-
`, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-xl-wide: 1280px;--val-container-md-wide: 900px;--val-container-aside: 0px;--val-container-padding: 16px;--val-radius-xs: 10px;--val-radius-sm: 16px;--val-radius-md: 20px;--val-radius-lg: 28px;--val-radius-xl: 36px;--val-radius-full: 999px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #737478;--ion-color-medium-rgb: 115,116,120;--ion-color-medium-contrast: #ffffff;--ion-color-medium-contrast-rgb: 255,255,255;--ion-color-medium-shade: #65666a;--ion-color-medium-tint: #818286;--ion-color-warning: #ffde38;--ion-color-warning-rgb: 255, 222, 56;--ion-color-warning-contrast: #000000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0c331;--ion-color-warning-tint: #ffe14c;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-dark)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}ion-select{--padding-top: 0;--padding-bottom: 0;--padding-start: 0;--padding-end: 0;min-height:auto;border:.0625rem solid var(--ion-color-medium);border-radius:1.5rem;margin-top:.375rem;padding-top:.5rem!important;padding-bottom:.5rem!important;padding-inline-start:1rem!important;padding-inline-end:.
|
|
23133
|
+
`, styles: ["@charset \"UTF-8\";:root{--val-container-sm: 540px;--val-container-md: 720px;--val-container-lg: 880px;--val-container-xl: 1100px;--val-container-xl-wide: 1280px;--val-container-md-wide: 900px;--val-container-aside: 0px;--val-container-padding: 16px;--val-radius-xs: 10px;--val-radius-sm: 16px;--val-radius-md: 20px;--val-radius-lg: 28px;--val-radius-xl: 36px;--val-radius-full: 999px;--ion-color-primary: #7026df;--ion-color-primary-rgb: 112, 38, 223;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #6321c4;--ion-color-primary-tint: #7e3ce2;--ion-color-secondary: #e2ccff;--ion-color-secondary-rgb: 226, 204, 255;--ion-color-secondary-contrast: #000000;--ion-color-secondary-contrast-rgb: 0, 0, 0;--ion-color-secondary-shade: #c7b4e0;--ion-color-secondary-tint: #e5d1ff;--ion-color-texti: #354c69;--ion-color-texti-rgb: 53, 76, 105;--ion-color-texti-contrast: #ffffff;--ion-color-texti-contrast-rgb: 255, 255, 255;--ion-color-texti-shade: #2f435c;--ion-color-texti-tint: #495e78;--ion-color-darki: #090f1b;--ion-color-darki-rgb: 9, 15, 27;--ion-color-darki-contrast: #ffffff;--ion-color-darki-contrast-rgb: 255, 255, 255;--ion-color-darki-shade: #080d18;--ion-color-darki-tint: #222732;--ion-color-medium: #737478;--ion-color-medium-rgb: 115,116,120;--ion-color-medium-contrast: #ffffff;--ion-color-medium-contrast-rgb: 255,255,255;--ion-color-medium-shade: #65666a;--ion-color-medium-tint: #818286;--ion-color-warning: #ffde38;--ion-color-warning-rgb: 255, 222, 56;--ion-color-warning-contrast: #000000;--ion-color-warning-contrast-rgb: 0, 0, 0;--ion-color-warning-shade: #e0c331;--ion-color-warning-tint: #ffe14c;--swiper-pagination-color: var(--ion-color-primary);--swiper-navigation-color: var(--ion-color-primary);--swiper-pagination-bullet-inactive-color: var(--ion-color-dark)}body.dark,html.ion-palette-dark,body[data-theme=dark]{--ion-color-texti: #8fc1ff;--ion-color-texti-rgb: 143, 193, 255;--ion-color-texti-contrast: #000000;--ion-color-texti-contrast-rgb: 0, 0, 0;--ion-color-texti-shade: #7eaae0;--ion-color-texti-tint: #9ac7ff;--ion-color-darki: #ffffff;--ion-color-darki-rgb: 255, 255, 255;--ion-color-darki-contrast: #000000;--ion-color-darki-contrast-rgb: 0, 0, 0;--ion-color-darki-shade: #e0e0e0;--ion-color-darki-tint: #ffffff;--ion-color-primary: #8f49f8;--ion-color-primary-rgb: 143, 73, 248;--ion-color-primary-contrast: #ffffff;--ion-color-primary-contrast-rgb: 255, 255, 255;--ion-color-primary-shade: #7e40da;--ion-color-primary-tint: #9a5bf9}.ion-color-texti{--ion-color-base: var(--ion-color-texti);--ion-color-base-rgb: var(--ion-color-texti-rgb);--ion-color-contrast: var(--ion-color-texti-contrast);--ion-color-contrast-rgb: var(--ion-color-texti-contrast-rgb);--ion-color-shade: var(--ion-color-texti-shade);--ion-color-tint: var(--ion-color-texti-tint)}.ion-color-darki{--ion-color-base: var(--ion-color-darki);--ion-color-base-rgb: var(--ion-color-darki-rgb);--ion-color-contrast: var(--ion-color-darki-contrast);--ion-color-contrast-rgb: var(--ion-color-darki-contrast-rgb);--ion-color-shade: var(--ion-color-darki-shade);--ion-color-tint: var(--ion-color-darki-tint)}ion-select{--padding-top: 0;--padding-bottom: 0;--padding-start: 0;--padding-end: 0;min-height:auto;border:.0625rem solid var(--ion-color-medium);border-radius:1.5rem;margin-top:.375rem;padding-top:.5rem!important;padding-bottom:.5rem!important;padding-inline-start:1rem!important;padding-inline-end:.875rem!important}.select-option-content{display:inline-flex;align-items:center;gap:.625rem}.select-option-icon{flex:0 0 auto;font-size:1.125rem}.select-option-icon-mask{width:1.125rem;height:1.125rem;background:currentColor;mask:var(--val-select-option-icon-src) center/contain no-repeat;-webkit-mask:var(--val-select-option-icon-src) center/contain no-repeat}\n"] }]
|
|
22822
23134
|
}], propDecorators: { preset: [{
|
|
22823
23135
|
type: Input
|
|
22824
23136
|
}], props: [{
|
|
@@ -37930,260 +38242,6 @@ const DEFAULT_FEEDBACK_TYPE_OPTIONS = [
|
|
|
37930
38242
|
*/
|
|
37931
38243
|
// Configuration
|
|
37932
38244
|
|
|
37933
|
-
/**
|
|
37934
|
-
* Default values for image processing
|
|
37935
|
-
*/
|
|
37936
|
-
const IMAGE_DEFAULTS = {
|
|
37937
|
-
maxWidth: 800,
|
|
37938
|
-
maxHeight: 800,
|
|
37939
|
-
quality: 0.8,
|
|
37940
|
-
mimeType: 'image/jpeg',
|
|
37941
|
-
maxSize: 10 * 1024 * 1024, // 10MB
|
|
37942
|
-
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
|
|
37943
|
-
thumbnailSize: 150,
|
|
37944
|
-
};
|
|
37945
|
-
|
|
37946
|
-
/**
|
|
37947
|
-
* ImageService
|
|
37948
|
-
*
|
|
37949
|
-
* Service for image processing including compression, thumbnails, cropping and validation.
|
|
37950
|
-
* Uses HTML Canvas for all operations - no external dependencies.
|
|
37951
|
-
*
|
|
37952
|
-
* @example
|
|
37953
|
-
* ```typescript
|
|
37954
|
-
* const imageService = inject(ImageService);
|
|
37955
|
-
*
|
|
37956
|
-
* // Compress an image
|
|
37957
|
-
* const compressed = await imageService.compress(file, { maxWidth: 800, quality: 0.8 });
|
|
37958
|
-
*
|
|
37959
|
-
* // Generate thumbnail
|
|
37960
|
-
* const thumb = await imageService.thumbnail(file, 150);
|
|
37961
|
-
*
|
|
37962
|
-
* // Validate before processing
|
|
37963
|
-
* const validation = imageService.validate(file, { maxSize: 5 * 1024 * 1024 });
|
|
37964
|
-
* if (!validation.valid) {
|
|
37965
|
-
* console.error(validation.message);
|
|
37966
|
-
* }
|
|
37967
|
-
* ```
|
|
37968
|
-
*/
|
|
37969
|
-
class ImageService {
|
|
37970
|
-
/**
|
|
37971
|
-
* Compress an image maintaining aspect ratio
|
|
37972
|
-
* @param file - File or Blob to compress
|
|
37973
|
-
* @param options - Compression options
|
|
37974
|
-
* @returns Promise with processed image data
|
|
37975
|
-
*/
|
|
37976
|
-
async compress(file, options) {
|
|
37977
|
-
const opts = {
|
|
37978
|
-
maxWidth: options?.maxWidth ?? IMAGE_DEFAULTS.maxWidth,
|
|
37979
|
-
maxHeight: options?.maxHeight ?? IMAGE_DEFAULTS.maxHeight,
|
|
37980
|
-
quality: options?.quality ?? IMAGE_DEFAULTS.quality,
|
|
37981
|
-
mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
|
|
37982
|
-
};
|
|
37983
|
-
const img = await this.loadImage(file);
|
|
37984
|
-
const { width, height } = this.calculateDimensions(img.width, img.height, opts.maxWidth, opts.maxHeight);
|
|
37985
|
-
const canvas = document.createElement('canvas');
|
|
37986
|
-
canvas.width = width;
|
|
37987
|
-
canvas.height = height;
|
|
37988
|
-
const ctx = canvas.getContext('2d');
|
|
37989
|
-
ctx.drawImage(img, 0, 0, width, height);
|
|
37990
|
-
const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
|
|
37991
|
-
const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
|
|
37992
|
-
return {
|
|
37993
|
-
blob,
|
|
37994
|
-
dataUrl,
|
|
37995
|
-
width,
|
|
37996
|
-
height,
|
|
37997
|
-
size: blob.size,
|
|
37998
|
-
};
|
|
37999
|
-
}
|
|
38000
|
-
/**
|
|
38001
|
-
* Generate a square thumbnail from an image
|
|
38002
|
-
* @param file - File or Blob to process
|
|
38003
|
-
* @param size - Thumbnail size in pixels (default: 150)
|
|
38004
|
-
* @returns Promise with processed thumbnail
|
|
38005
|
-
*/
|
|
38006
|
-
async thumbnail(file, size) {
|
|
38007
|
-
const thumbSize = size ?? IMAGE_DEFAULTS.thumbnailSize;
|
|
38008
|
-
const img = await this.loadImage(file);
|
|
38009
|
-
// Calculate square crop from center
|
|
38010
|
-
const minDim = Math.min(img.width, img.height);
|
|
38011
|
-
const cropX = (img.width - minDim) / 2;
|
|
38012
|
-
const cropY = (img.height - minDim) / 2;
|
|
38013
|
-
const canvas = document.createElement('canvas');
|
|
38014
|
-
canvas.width = thumbSize;
|
|
38015
|
-
canvas.height = thumbSize;
|
|
38016
|
-
const ctx = canvas.getContext('2d');
|
|
38017
|
-
ctx.drawImage(img, cropX, cropY, minDim, minDim, 0, 0, thumbSize, thumbSize);
|
|
38018
|
-
const blob = await this.canvasToBlob(canvas, IMAGE_DEFAULTS.mimeType, 0.7 // Lower quality for thumbnails
|
|
38019
|
-
);
|
|
38020
|
-
const dataUrl = canvas.toDataURL(IMAGE_DEFAULTS.mimeType, 0.7);
|
|
38021
|
-
return {
|
|
38022
|
-
blob,
|
|
38023
|
-
dataUrl,
|
|
38024
|
-
width: thumbSize,
|
|
38025
|
-
height: thumbSize,
|
|
38026
|
-
size: blob.size,
|
|
38027
|
-
};
|
|
38028
|
-
}
|
|
38029
|
-
/**
|
|
38030
|
-
* Crop an image with specific coordinates
|
|
38031
|
-
* @param file - File or Blob to crop
|
|
38032
|
-
* @param cropData - Crop coordinates and dimensions
|
|
38033
|
-
* @param options - Optional compression options for output
|
|
38034
|
-
* @returns Promise with cropped image
|
|
38035
|
-
*/
|
|
38036
|
-
async crop(file, cropData, options) {
|
|
38037
|
-
const img = await this.loadImage(file);
|
|
38038
|
-
const opts = {
|
|
38039
|
-
quality: options?.quality ?? IMAGE_DEFAULTS.quality,
|
|
38040
|
-
mimeType: options?.mimeType ?? IMAGE_DEFAULTS.mimeType,
|
|
38041
|
-
};
|
|
38042
|
-
const canvas = document.createElement('canvas');
|
|
38043
|
-
canvas.width = cropData.width;
|
|
38044
|
-
canvas.height = cropData.height;
|
|
38045
|
-
const ctx = canvas.getContext('2d');
|
|
38046
|
-
ctx.drawImage(img, cropData.x, cropData.y, cropData.width, cropData.height, 0, 0, cropData.width, cropData.height);
|
|
38047
|
-
// Apply max dimensions if specified
|
|
38048
|
-
if (options?.maxWidth || options?.maxHeight) {
|
|
38049
|
-
return this.compress(await this.canvasToBlob(canvas, opts.mimeType, 1), options);
|
|
38050
|
-
}
|
|
38051
|
-
const blob = await this.canvasToBlob(canvas, opts.mimeType, opts.quality);
|
|
38052
|
-
const dataUrl = canvas.toDataURL(opts.mimeType, opts.quality);
|
|
38053
|
-
return {
|
|
38054
|
-
blob,
|
|
38055
|
-
dataUrl,
|
|
38056
|
-
width: cropData.width,
|
|
38057
|
-
height: cropData.height,
|
|
38058
|
-
size: blob.size,
|
|
38059
|
-
};
|
|
38060
|
-
}
|
|
38061
|
-
/**
|
|
38062
|
-
* Validate an image file before processing
|
|
38063
|
-
* @param file - File to validate
|
|
38064
|
-
* @param options - Validation options
|
|
38065
|
-
* @returns Validation result with error details if invalid
|
|
38066
|
-
*/
|
|
38067
|
-
validate(file, options) {
|
|
38068
|
-
const opts = {
|
|
38069
|
-
maxSize: options?.maxSize ?? IMAGE_DEFAULTS.maxSize,
|
|
38070
|
-
allowedTypes: options?.allowedTypes ?? IMAGE_DEFAULTS.allowedTypes,
|
|
38071
|
-
};
|
|
38072
|
-
// Check file type
|
|
38073
|
-
if (!opts.allowedTypes.includes(file.type)) {
|
|
38074
|
-
return {
|
|
38075
|
-
valid: false,
|
|
38076
|
-
error: 'invalidType',
|
|
38077
|
-
message: `Formato no válido. Usa: ${opts.allowedTypes.map(t => t.split('/')[1].toUpperCase()).join(', ')}`,
|
|
38078
|
-
};
|
|
38079
|
-
}
|
|
38080
|
-
// Check file size
|
|
38081
|
-
if (file.size > opts.maxSize) {
|
|
38082
|
-
const maxMB = Math.round(opts.maxSize / (1024 * 1024));
|
|
38083
|
-
return {
|
|
38084
|
-
valid: false,
|
|
38085
|
-
error: 'fileTooLarge',
|
|
38086
|
-
message: `La imagen es muy grande. Máximo ${maxMB}MB`,
|
|
38087
|
-
};
|
|
38088
|
-
}
|
|
38089
|
-
return { valid: true };
|
|
38090
|
-
}
|
|
38091
|
-
/**
|
|
38092
|
-
* Validate image dimensions (async - requires loading image)
|
|
38093
|
-
* @param file - File to validate
|
|
38094
|
-
* @param options - Validation options with minWidth/minHeight
|
|
38095
|
-
* @returns Promise with validation result
|
|
38096
|
-
*/
|
|
38097
|
-
async validateDimensions(file, options) {
|
|
38098
|
-
const img = await this.loadImage(file);
|
|
38099
|
-
if (options.minWidth && img.width < options.minWidth) {
|
|
38100
|
-
return {
|
|
38101
|
-
valid: false,
|
|
38102
|
-
error: 'imageTooSmall',
|
|
38103
|
-
message: `La imagen debe tener al menos ${options.minWidth}px de ancho`,
|
|
38104
|
-
};
|
|
38105
|
-
}
|
|
38106
|
-
if (options.minHeight && img.height < options.minHeight) {
|
|
38107
|
-
return {
|
|
38108
|
-
valid: false,
|
|
38109
|
-
error: 'imageTooSmall',
|
|
38110
|
-
message: `La imagen debe tener al menos ${options.minHeight}px de alto`,
|
|
38111
|
-
};
|
|
38112
|
-
}
|
|
38113
|
-
return { valid: true };
|
|
38114
|
-
}
|
|
38115
|
-
/**
|
|
38116
|
-
* Convert a Blob/File to a data URL
|
|
38117
|
-
*/
|
|
38118
|
-
async toDataUrl(file) {
|
|
38119
|
-
return new Promise((resolve, reject) => {
|
|
38120
|
-
const reader = new FileReader();
|
|
38121
|
-
reader.onload = () => resolve(reader.result);
|
|
38122
|
-
reader.onerror = reject;
|
|
38123
|
-
reader.readAsDataURL(file);
|
|
38124
|
-
});
|
|
38125
|
-
}
|
|
38126
|
-
/**
|
|
38127
|
-
* Convert a data URL to a Blob
|
|
38128
|
-
*/
|
|
38129
|
-
dataUrlToBlob(dataUrl) {
|
|
38130
|
-
const arr = dataUrl.split(',');
|
|
38131
|
-
const mime = arr[0].match(/:(.*?);/)[1];
|
|
38132
|
-
const bstr = atob(arr[1]);
|
|
38133
|
-
let n = bstr.length;
|
|
38134
|
-
const u8arr = new Uint8Array(n);
|
|
38135
|
-
while (n--) {
|
|
38136
|
-
u8arr[n] = bstr.charCodeAt(n);
|
|
38137
|
-
}
|
|
38138
|
-
return new Blob([u8arr], { type: mime });
|
|
38139
|
-
}
|
|
38140
|
-
// ============== Private Helpers ==============
|
|
38141
|
-
loadImage(file) {
|
|
38142
|
-
return new Promise((resolve, reject) => {
|
|
38143
|
-
const img = new Image();
|
|
38144
|
-
img.onload = () => {
|
|
38145
|
-
URL.revokeObjectURL(img.src);
|
|
38146
|
-
resolve(img);
|
|
38147
|
-
};
|
|
38148
|
-
img.onerror = reject;
|
|
38149
|
-
img.src = URL.createObjectURL(file);
|
|
38150
|
-
});
|
|
38151
|
-
}
|
|
38152
|
-
calculateDimensions(originalWidth, originalHeight, maxWidth, maxHeight) {
|
|
38153
|
-
let width = originalWidth;
|
|
38154
|
-
let height = originalHeight;
|
|
38155
|
-
// Scale down if necessary, maintaining aspect ratio
|
|
38156
|
-
if (width > maxWidth) {
|
|
38157
|
-
height = (height * maxWidth) / width;
|
|
38158
|
-
width = maxWidth;
|
|
38159
|
-
}
|
|
38160
|
-
if (height > maxHeight) {
|
|
38161
|
-
width = (width * maxHeight) / height;
|
|
38162
|
-
height = maxHeight;
|
|
38163
|
-
}
|
|
38164
|
-
return {
|
|
38165
|
-
width: Math.round(width),
|
|
38166
|
-
height: Math.round(height),
|
|
38167
|
-
};
|
|
38168
|
-
}
|
|
38169
|
-
canvasToBlob(canvas, mimeType, quality) {
|
|
38170
|
-
return new Promise((resolve, reject) => {
|
|
38171
|
-
canvas.toBlob((blob) => {
|
|
38172
|
-
if (blob)
|
|
38173
|
-
resolve(blob);
|
|
38174
|
-
else
|
|
38175
|
-
reject(new Error('Failed to create blob from canvas'));
|
|
38176
|
-
}, mimeType, quality);
|
|
38177
|
-
});
|
|
38178
|
-
}
|
|
38179
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
38180
|
-
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, providedIn: 'root' }); }
|
|
38181
|
-
}
|
|
38182
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ImageService, decorators: [{
|
|
38183
|
-
type: Injectable,
|
|
38184
|
-
args: [{ providedIn: 'root' }]
|
|
38185
|
-
}] });
|
|
38186
|
-
|
|
38187
38245
|
class AttachmentUploaderComponent {
|
|
38188
38246
|
get readyUrls() {
|
|
38189
38247
|
return this.attachments()
|
|
@@ -59765,9 +59823,13 @@ class PreferencesService {
|
|
|
59765
59823
|
.docChanges(`users/${userId}/preferences`, 'main')
|
|
59766
59824
|
.subscribe(doc => {
|
|
59767
59825
|
if (!doc) {
|
|
59768
|
-
// Doc no existe aún
|
|
59769
|
-
//
|
|
59770
|
-
//
|
|
59826
|
+
// Doc no existe aún para esta app. Antes dejábamos la app con
|
|
59827
|
+
// localStorage/navigator, aunque el usuario ya tuviera idioma
|
|
59828
|
+
// canónico en auth. Resultado: cuenta en español, app nueva en inglés
|
|
59829
|
+
// hasta cambiar manualmente a inglés y volver a español. Hidratar
|
|
59830
|
+
// desde perfil canónico corrige ese primer arranque y además crea el
|
|
59831
|
+
// mirror de esta app.
|
|
59832
|
+
this.hydrateMissingPreferencesFromProfile(userId);
|
|
59771
59833
|
return;
|
|
59772
59834
|
}
|
|
59773
59835
|
// Ignorar emisiones del listener mientras hay un update() en vuelo: un
|
|
@@ -59792,8 +59854,25 @@ class PreferencesService {
|
|
|
59792
59854
|
this.subscription?.unsubscribe();
|
|
59793
59855
|
this.subscription = undefined;
|
|
59794
59856
|
this.currentUserId = undefined;
|
|
59857
|
+
this.missingDocHydrationUserId = undefined;
|
|
59795
59858
|
this._synced.set(false);
|
|
59796
59859
|
}
|
|
59860
|
+
hydrateMissingPreferencesFromProfile(userId) {
|
|
59861
|
+
if (this.missingDocHydrationUserId === userId)
|
|
59862
|
+
return;
|
|
59863
|
+
this.missingDocHydrationUserId = userId;
|
|
59864
|
+
void firstValueFrom(this.auth.getProfile())
|
|
59865
|
+
.then(profile => {
|
|
59866
|
+
const language = profile.language;
|
|
59867
|
+
if (language === 'es' || language === 'en') {
|
|
59868
|
+
this._language.set(language);
|
|
59869
|
+
this.i18n?.setLanguage(language);
|
|
59870
|
+
this._synced.set(true);
|
|
59871
|
+
void this.update({ language }).catch(() => undefined);
|
|
59872
|
+
}
|
|
59873
|
+
})
|
|
59874
|
+
.catch(() => undefined);
|
|
59875
|
+
}
|
|
59797
59876
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PreferencesService, deps: [{ token: VALTECH_AUTH_CONFIG }, { token: FirestoreService }, { token: AuthService }, { token: i1$3.HttpClient }, { token: FirebaseService, optional: true }, { token: ThemeService, optional: true }, { token: FontSizeService, optional: true }, { token: I18nService, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
59798
59877
|
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: PreferencesService, providedIn: 'root' }); }
|
|
59799
59878
|
}
|