@sumaris-net/ngx-components 21.4.0 → 21.4.1

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.
@@ -33202,268 +33202,137 @@ class CsvUtils {
33202
33202
  }
33203
33203
  }
33204
33204
 
33205
- /**
33206
- * Define here theme colors
33207
- */
33208
- const rgbArrayMap = {
33209
- white: [255, 255, 255],
33210
- primary: [20, 67, 145], // ok
33211
- secondary: [117, 196, 253], // ok
33212
- tertiary: [91, 94, 244], // ok
33213
- danger: [245, 61, 61], // ok
33214
- light: [244, 245, 248], // ok
33215
- medium: [152, 154, 162], // ok
33216
- dark: [34, 36, 40], // ok
33217
- red: [255, 0, 0],
33218
- green: [0, 255, 0],
33219
- blue: [0, 0, 255],
33220
- };
33221
- // Fill a map of Color objects
33222
- const colorsMap = {};
33223
- /**
33224
- * Useful class for color conversion
33225
- */
33226
- // @dynamic
33227
- class Color {
33228
- _rgbArray;
33229
- _opacity;
33230
- _name;
33231
- // Helper method, to retrieve a color
33232
- static get(name) {
33233
- return colorsMap[name];
33234
- }
33235
- static parseRgba(rgba) {
33236
- if (!rgba || (!rgba.startsWith('rgb(') && !rgba.startsWith('rgba(')))
33237
- return null;
33238
- // Parse parts
33239
- const parts = rgba.replace('rgb(', '').replace('rgba(', '').replace(')', '').split(',');
33240
- if (parts.length !== 3 && parts.length !== 4)
33241
- return null;
33242
- return new Color([+parts[0], +parts[1], +parts[2]], (parts.length === 4 && +parts[3]) || 1, 'custom');
33243
- }
33244
- static transparent = function () {
33245
- return new Color([0, 0, 0], 0, 'translucent');
33246
- };
33247
- constructor(_rgbArray, _opacity, _name = 'custom') {
33248
- this._rgbArray = _rgbArray;
33249
- this._opacity = _opacity;
33250
- this._name = _name;
33251
- }
33252
- get name() {
33253
- return this._name;
33254
- }
33255
- get opacity() {
33256
- return isNotNil(this._opacity) ? this._opacity : 1;
33257
- }
33258
- get rgb() {
33259
- return this._rgbArray;
33260
- }
33261
- get r() {
33262
- return this._rgbArray[0];
33263
- }
33264
- get g() {
33265
- return this._rgbArray[1];
33266
- }
33267
- get b() {
33268
- return this._rgbArray[2];
33205
+ // GPL v3 License
33206
+ class FileAttachmentUtils {
33207
+ /**
33208
+ * Get the file name of an attachment (its name, or the last part of its URL)
33209
+ */
33210
+ static getFilename(attachment) {
33211
+ if (attachment?.name?.trim())
33212
+ return attachment.name.trim();
33213
+ const path = attachment?.url?.split('?')[0];
33214
+ return path?.substring(path.lastIndexOf('/') + 1) || 'attachment';
33269
33215
  }
33270
- rgba(opacity) {
33271
- opacity = isNotNil(opacity) ? opacity : this._opacity;
33272
- if (isNil(opacity) || opacity < 0 || opacity > 1) {
33273
- return 'rgb(' + this._rgbArray.join(',') + ')';
33274
- }
33275
- return 'rgba(' + this._rgbArray.join(',') + ',' + opacity + ')';
33216
+ /**
33217
+ * Get the Material icon (font ligature) of an attachment
33218
+ */
33219
+ static getIcon(attachment) {
33220
+ return FileTypeUtils.getIconByFile({ name: FileAttachmentUtils.getFilename(attachment), type: attachment?.mimeType });
33276
33221
  }
33277
33222
  }
33223
+
33224
+ // GPL v3 License
33225
+ /** Kinds of file that can be previewed (displayed without being downloaded) */
33226
+ const PREVIEW_KINDS = Object.freeze(['image', 'text', 'code']);
33227
+ /** Extra MIME types (or extensions) that can be previewed as text */
33228
+ const PREVIEW_TEXT_MIME_TYPES = /^(text\/(csv|tab-separated-values|markdown|x-markdown)|application\/csv)$/;
33229
+ const PREVIEW_TEXT_EXTENSIONS = Object.freeze(['csv', 'tsv', 'md', 'markdown']);
33230
+ const MARKDOWN_MIME_TYPES = /^text\/(x-)?markdown$/;
33231
+ const MARKDOWN_EXTENSIONS = Object.freeze(['md', 'markdown']);
33232
+ const HTML_MIME_TYPES = /^(text\/html|application\/xhtml\+xml)$/;
33233
+ const HTML_EXTENSIONS = Object.freeze(['html', 'htm', 'xhtml']);
33278
33234
  /**
33279
- * Helper class for colors scale
33235
+ * Download (or read) a remote file, sending the application headers (e.g. authorization) when the file is on the current peer.
33236
+ *
33237
+ * - On Android (Cordova downloader): the file is downloaded by the native download manager (visible in the "Downloads" app);
33238
+ * - Otherwise (web, iOS): the file is fetched as a Blob, then saved using a temporary object URL.
33280
33239
  */
33281
- // @dynamic
33282
- class ColorScale {
33283
- static custom = (count, options) => {
33284
- options = options || {};
33285
- return new ColorScale(linearColorGradientWithIntermediate(count, {
33286
- startColor: options.startColor || undefined,
33287
- mainColor: options.mainColor || undefined,
33288
- mainColorIndex: isNotNil(options.mainColorIndex) ? options.mainColorIndex : undefined,
33289
- endColor: options.endColor || undefined,
33290
- }), options);
33291
- };
33292
- static default() {
33293
- return ColorScale.custom(25);
33294
- }
33240
+ class FileDownloadService {
33241
+ network = inject(NetworkService);
33242
+ platform = inject(PlatformService);
33295
33243
  /**
33296
- * Create an array with the given color
33297
- **/
33298
- static fix(length, colorName) {
33299
- const color = Color.get(colorName || 'primary').rgba(0.5);
33300
- return new Array(length || 25).fill(color);
33301
- }
33302
- _min;
33303
- _max;
33304
- _rangeSize;
33305
- _legendItems;
33306
- get min() {
33307
- return this._min;
33308
- }
33309
- get max() {
33310
- return this._max;
33244
+ * Can the file be previewed (displayed without being downloaded)?
33245
+ */
33246
+ canPreview(file) {
33247
+ return this.isImage(file) || this.isText(file);
33311
33248
  }
33312
- constructor(colorArray, options) {
33313
- options = options || {};
33314
- // reserved last colors for value > max
33315
- const nbIntervalBeforeUpper = !options.upperMax ? colorArray.length : colorArray.length - 1;
33316
- this._min = options.min || 0;
33317
- this._max = options.max || nbIntervalBeforeUpper;
33318
- this._rangeSize = Math.round((this._max - this._min) / nbIntervalBeforeUpper);
33319
- this._legendItems = this.computeLegendItems(colorArray);
33249
+ isImage(file) {
33250
+ return FileTypeUtils.getKindByFile({ name: file?.name, type: file?.mimeType }) === 'image';
33320
33251
  }
33321
- get legend() {
33322
- return {
33323
- items: this._legendItems,
33324
- };
33252
+ isText(file) {
33253
+ const mimeType = file?.mimeType?.trim().toLowerCase();
33254
+ if (mimeType && PREVIEW_TEXT_MIME_TYPES.test(mimeType))
33255
+ return true;
33256
+ if (PREVIEW_TEXT_EXTENSIONS.includes(FileTypeUtils.getExtension(file?.name)))
33257
+ return true;
33258
+ const kind = FileTypeUtils.getKindByFile({ name: file?.name, type: mimeType });
33259
+ return kind !== 'image' && PREVIEW_KINDS.includes(kind);
33325
33260
  }
33326
- getValueColor(value) {
33327
- return this.getLegendForValue(value).color;
33261
+ isMarkdown(file) {
33262
+ const mimeType = file?.mimeType?.trim().toLowerCase();
33263
+ return (mimeType && MARKDOWN_MIME_TYPES.test(mimeType)) || MARKDOWN_EXTENSIONS.includes(FileTypeUtils.getExtension(file?.name));
33328
33264
  }
33329
- getLegendForValue(value) {
33330
- if (value <= this._min)
33331
- return this._legendItems[0];
33332
- if (value >= this._max)
33333
- return this._legendItems[this._legendItems.length - 1];
33334
- const index = Math.floor((value * (this._legendItems.length - 1)) / this._max);
33335
- return this._legendItems[index];
33265
+ isHtml(file) {
33266
+ const mimeType = file?.mimeType?.trim().toLowerCase();
33267
+ return (mimeType && HTML_MIME_TYPES.test(mimeType)) || HTML_EXTENSIONS.includes(FileTypeUtils.getExtension(file?.name));
33336
33268
  }
33337
- getLegendAtIndex(index) {
33338
- return this._legendItems[index];
33269
+ /**
33270
+ * Fetch a remote file, as a Blob
33271
+ */
33272
+ getBlob(url) {
33273
+ return this.network.getBlob(FileDownloadService.normalizeUrl(url));
33339
33274
  }
33340
- computeLegendItems(colorArray) {
33341
- return colorArray.map((color, index) => {
33342
- const start = index * this._rangeSize;
33343
- const end = start + this._rangeSize;
33344
- return {
33345
- color: new Color(color),
33346
- label: end < this._max ? `${start.toLocaleString()} - ${end.toLocaleString()}` : ` >= ${start}`,
33347
- };
33348
- });
33275
+ /**
33276
+ * Remove duplicated slashes in the path of an URL (e.g. 'http://host//api/file' => 'http://host/api/file').
33277
+ * Needed because the server (and its CORS rules) does not match a path with duplicated slashes.
33278
+ */
33279
+ static normalizeUrl(url) {
33280
+ if (!url)
33281
+ return url;
33282
+ const [path, ...query] = url.split('?');
33283
+ const normalizedPath = path.replace(/([^:\/])\/{2,}/g, '$1/');
33284
+ return [normalizedPath, ...query].join('?');
33349
33285
  }
33350
- }
33351
- // Fill colorsMap
33352
- Object.getOwnPropertyNames(rgbArrayMap).forEach((key) => {
33353
- colorsMap[key] = new Color(rgbArrayMap[key], 1, key);
33354
- });
33355
- /**
33356
- * Internal function, that create a colors scale, using iteration
33357
- *
33358
- * @param count
33359
- * @param opacity
33360
- * @param startColor
33361
- * @param startState
33362
- * @returns
33363
- */
33364
- function linearColorGradientWithIntermediate(count, options) {
33365
- options = options || {};
33366
- // From [0,1]
33367
- options.startColor = options.startColor || [255, 255, 190]; // default start = creme
33368
- options.mainColorIndex = options.mainColorIndex && options.mainColorIndex < count - 1 ? options.mainColorIndex : count - 1;
33369
- options.endColor = options.endColor || [255, 0, 0]; // default main = red
33370
- if (!options.mainColor) {
33371
- return linearColorGradient(count, {
33372
- startColor: options.startColor,
33373
- endColor: options.endColor,
33374
- });
33286
+ /**
33287
+ * Fetch a remote file, as a text (e.g. to preview it)
33288
+ */
33289
+ async getText(url) {
33290
+ const blob = await this.getBlob(url);
33291
+ return blob.text();
33375
33292
  }
33376
- else {
33377
- // Step 1: startColor -> mainColor
33378
- const result = linearColorGradient(options.mainColorIndex + 1, {
33379
- startColor: options.startColor,
33380
- endColor: options.mainColor,
33381
- });
33382
- // Step 2: mainColor -> endColor
33383
- if (options.mainColorIndex < count - 1) {
33384
- return result.concat(linearColorGradient(count - options.mainColorIndex, {
33385
- startColor: options.mainColor,
33386
- endColor: options.endColor,
33293
+ /**
33294
+ * Download a remote file
33295
+ */
33296
+ async download(url, opts) {
33297
+ if (!url)
33298
+ throw new Error("Missing argument 'url'");
33299
+ url = FileDownloadService.normalizeUrl(url);
33300
+ const filename = opts?.filename || this.getFilename(url);
33301
+ // Android: use the native download manager
33302
+ if (this.platform.canDownload) {
33303
+ const headers = Object.entries(this.network.getAppHeaders(url)).map(([header, value]) => ({
33304
+ header,
33305
+ value: Array.isArray(value) ? value.join(', ') : value,
33387
33306
  }));
33307
+ await firstValueFrom(this.platform.download({ uri: url, filename, title: filename, mimeType: opts?.mimeType, headers }));
33308
+ return;
33388
33309
  }
33389
- else {
33390
- return result;
33310
+ // Web, iOS: fetch as a Blob (with the authorization headers), then save it
33311
+ const blob = await this.getBlob(url);
33312
+ const objectUrl = URL.createObjectURL(blob);
33313
+ try {
33314
+ FilesUtils.downloadUri(objectUrl, filename);
33391
33315
  }
33392
- }
33393
- }
33394
- function linearColorGradient(count, options) {
33395
- options = options || {};
33396
- // From [0,1]
33397
- options.startColor = options.startColor || [255, 255, 255]; // default start = white
33398
- options.endColor = options.endColor || [255, 0, 0]; // default end = red
33399
- const result = [];
33400
- const color = options.startColor.slice(); // copy the start color
33401
- const delta = [
33402
- Math.round((options.endColor[0] - options.startColor[0]) / count),
33403
- Math.round((options.endColor[1] - options.startColor[1]) / count),
33404
- Math.round((options.endColor[2] - options.startColor[2]) / count),
33405
- ];
33406
- for (let i = 0; i < count - 1; i++) {
33407
- for (let j = 0; j < 3; j++) {
33408
- color[j] += delta[j];
33316
+ finally {
33317
+ // Release the object URL, once the download has started
33318
+ setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
33409
33319
  }
33410
- result.push(color.slice());
33411
- }
33412
- // Force last color = end color
33413
- result.push(options.endColor.slice());
33414
- // Output as array
33415
- return result;
33416
- }
33417
-
33418
- class DebugComponent {
33419
- titlePrefix = 'COMMON.DEBUG.TITLE';
33420
- title = '';
33421
- enable = null;
33422
- expanded = false;
33423
- expansionPanel;
33424
- constructor(environment) {
33425
- this.enable = !environment || !environment.production;
33426
33320
  }
33427
- toggle() {
33428
- this.expansionPanel.toggle();
33321
+ /* -- protected methods -- */
33322
+ getFilename(url) {
33323
+ try {
33324
+ return FilesUtils.getFilenameFromUri(url.split('?')[0]) || 'download';
33325
+ }
33326
+ catch {
33327
+ return 'download';
33328
+ }
33429
33329
  }
33430
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DebugComponent, deps: [{ token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Component });
33431
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: DebugComponent, isStandalone: false, selector: "app-debug", inputs: { titlePrefix: "titlePrefix", title: "title", enable: "enable", expanded: "expanded" }, viewQueries: [{ propertyName: "expansionPanel", first: true, predicate: ["expansionPanel"], descendants: true }], ngImport: i0, template: "<!-- debug -->\n<ion-item tappable (click)=\"toggle()\" lines=\"none\" color=\"secondary50\">\n <ion-icon slot=\"start\" name=\"bug\"></ion-icon>\n <ion-label color=\"primary\">{{ titlePrefix | translate }} {{ title }}</ion-label>\n <mat-icon slot=\"end\">{{ expansionPanel.expanded ? 'expand_less' : 'expand_more' }}</mat-icon>\n</ion-item>\n<mat-expansion-panel #expansionPanel [(expanded)]=\"expanded\">\n <ion-text color=\"primary100\">\n <div class=\"ion-margin-start\">\n <ng-content></ng-content>\n </div>\n </ion-text>\n</mat-expansion-panel>\n", styles: ["ion-item mat-icon,ion-item ion-icon{color:var(--ion-color-primary)}ion-item{cursor:pointer!important}.mat-expansion-panel{--expansion-panel-padding: 4px;background:var(--ion-color-secondary50)!important}.mat-expansion-panel.mat-expanded{margin-bottom:4px!important}.mat-expansion-panel .mat-expansion-panel-body *{color:var(--ion-color-primary100)}\n"], dependencies: [{ kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i11$2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }] });
33432
- }
33433
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DebugComponent, decorators: [{
33434
- type: Component,
33435
- args: [{ selector: 'app-debug', standalone: false, template: "<!-- debug -->\n<ion-item tappable (click)=\"toggle()\" lines=\"none\" color=\"secondary50\">\n <ion-icon slot=\"start\" name=\"bug\"></ion-icon>\n <ion-label color=\"primary\">{{ titlePrefix | translate }} {{ title }}</ion-label>\n <mat-icon slot=\"end\">{{ expansionPanel.expanded ? 'expand_less' : 'expand_more' }}</mat-icon>\n</ion-item>\n<mat-expansion-panel #expansionPanel [(expanded)]=\"expanded\">\n <ion-text color=\"primary100\">\n <div class=\"ion-margin-start\">\n <ng-content></ng-content>\n </div>\n </ion-text>\n</mat-expansion-panel>\n", styles: ["ion-item mat-icon,ion-item ion-icon{color:var(--ion-color-primary)}ion-item{cursor:pointer!important}.mat-expansion-panel{--expansion-panel-padding: 4px;background:var(--ion-color-secondary50)!important}.mat-expansion-panel.mat-expanded{margin-bottom:4px!important}.mat-expansion-panel .mat-expansion-panel-body *{color:var(--ion-color-primary100)}\n"] }]
33436
- }], ctorParameters: () => [{ type: Environment, decorators: [{
33437
- type: Inject,
33438
- args: [ENVIRONMENT]
33439
- }, {
33440
- type: Optional
33441
- }] }], propDecorators: { titlePrefix: [{
33442
- type: Input
33443
- }], title: [{
33444
- type: Input
33445
- }], enable: [{
33446
- type: Input
33447
- }], expanded: [{
33448
- type: Input
33449
- }], expansionPanel: [{
33450
- type: ViewChild,
33451
- args: ['expansionPanel']
33452
- }] } });
33453
-
33454
- class SharedDebugModule {
33455
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedDebugModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
33456
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: SharedDebugModule, declarations: [DebugComponent], imports: [CommonModule, SharedIonicModule, MatExpansionModule, MatIconModule, TranslatePipe], exports: [DebugComponent] });
33457
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedDebugModule, providers: [provideChildTranslateService()], imports: [CommonModule, SharedIonicModule, MatExpansionModule, MatIconModule] });
33330
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileDownloadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
33331
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileDownloadService, providedIn: 'root' });
33458
33332
  }
33459
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedDebugModule, decorators: [{
33460
- type: NgModule,
33461
- args: [{
33462
- imports: [CommonModule, SharedIonicModule, MatExpansionModule, MatIconModule, TranslatePipe],
33463
- declarations: [DebugComponent],
33464
- exports: [DebugComponent],
33465
- providers: [provideChildTranslateService()],
33466
- }]
33333
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileDownloadService, decorators: [{
33334
+ type: Injectable,
33335
+ args: [{ providedIn: 'root' }]
33467
33336
  }] });
33468
33337
 
33469
33338
  class GitlabUtils {
@@ -33929,6 +33798,155 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
33929
33798
  args: ['ready']
33930
33799
  }] } });
33931
33800
 
33801
+ // GPL v3 License
33802
+ /**
33803
+ * Display the content of an attachment (text, markdown, image, HTML), without downloading it
33804
+ */
33805
+ class FileAttachmentPreviewModal {
33806
+ cd = inject(ChangeDetectorRef);
33807
+ modalCtrl = inject(ModalController);
33808
+ fileDownload = inject(FileDownloadService);
33809
+ mobile = inject(LocalSettingsService).mobile;
33810
+ sanitizer = inject(DomSanitizer);
33811
+ elementRef = inject(ElementRef);
33812
+ attachment;
33813
+ loading = true;
33814
+ error;
33815
+ filename;
33816
+ text;
33817
+ markdown = false;
33818
+ imageUrl;
33819
+ /** HTML content, displayed in a sandboxed iframe (scripts allowed, but isolated from the application: no same origin) */
33820
+ html;
33821
+ /** Display the modal in a large size */
33822
+ maximized = false;
33823
+ async ngOnInit() {
33824
+ this.filename = FileAttachmentUtils.getFilename(this.attachment);
33825
+ const file = { name: this.filename, mimeType: this.attachment?.mimeType };
33826
+ try {
33827
+ if (this.fileDownload.isImage(file)) {
33828
+ const blob = await this.fileDownload.getBlob(this.attachment.url);
33829
+ this.imageUrl = URL.createObjectURL(blob);
33830
+ }
33831
+ else if (this.fileDownload.isHtml(file)) {
33832
+ const content = await this.fileDownload.getText(this.attachment.url);
33833
+ // Safe: the content is only rendered inside a sandboxed iframe (see template)
33834
+ this.html = this.sanitizer.bypassSecurityTrustHtml(content);
33835
+ }
33836
+ else {
33837
+ this.markdown = this.fileDownload.isMarkdown(file);
33838
+ this.text = await this.fileDownload.getText(this.attachment.url);
33839
+ }
33840
+ }
33841
+ catch (err) {
33842
+ console.error(`[file-attachment] Cannot load attachment {${this.attachment?.url}}`, err);
33843
+ this.error = 'FILE.ATTACHMENT.PREVIEW_ERROR';
33844
+ }
33845
+ finally {
33846
+ this.loading = false;
33847
+ this.cd.markForCheck();
33848
+ }
33849
+ }
33850
+ ngOnDestroy() {
33851
+ if (this.imageUrl)
33852
+ URL.revokeObjectURL(this.imageUrl);
33853
+ }
33854
+ async download() {
33855
+ await this.fileDownload.download(this.attachment.url, { filename: this.filename, mimeType: this.attachment.mimeType });
33856
+ }
33857
+ /** Display the modal in a large size (to restore the size: close then reopen the attachment) */
33858
+ maximize() {
33859
+ this.maximized = true;
33860
+ this.elementRef.nativeElement.closest('ion-modal')?.classList.add('modal-large');
33861
+ this.cd.markForCheck();
33862
+ }
33863
+ close() {
33864
+ return this.modalCtrl.dismiss();
33865
+ }
33866
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileAttachmentPreviewModal, deps: [], target: i0.ɵɵFactoryTarget.Component });
33867
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FileAttachmentPreviewModal, isStandalone: false, selector: "app-file-attachment-preview-modal", inputs: { attachment: "attachment" }, ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ filename }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"download-button\"\n (click)=\"download()\"\n [title]=\"'FILE.ATTACHMENT.BTN_DOWNLOAD' | translate\"\n >\n <mat-icon slot=\"icon-only\">download</mat-icon>\n </ion-button>\n @if (!mobile && !maximized) {\n <ion-button\n class=\"maximize-button\"\n (click)=\"maximize()\"\n [title]=\"'FILE.ATTACHMENT.BTN_MAXIMIZE' | translate\"\n >\n <mat-icon slot=\"icon-only\">fullscreen</mat-icon>\n </ion-button>\n }\n <ion-button class=\"close-button\" (click)=\"close()\" [title]=\"'COMMON.BTN_CLOSE' | translate\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n @if (loading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else if (error) {\n <ion-text color=\"danger\" class=\"preview-error\">{{ error | translate }}</ion-text>\n } @else if (imageUrl) {\n <img class=\"preview-image\" [src]=\"imageUrl\" [alt]=\"filename\" />\n } @else if (html) {\n <!-- sandboxed: scripts allowed, but no access to the application (no same origin) -->\n <iframe class=\"preview-html\" sandbox=\"allow-scripts\" [srcdoc]=\"html\" [title]=\"filename\"></iframe>\n } @else if (markdown) {\n <markdown class=\"preview-markdown\" [data]=\"text\"></markdown>\n } @else {\n <pre class=\"preview-text\">{{ text }}</pre>\n }\n</ion-content>\n", styles: [".preview-image{display:block;max-width:100%;margin:0 auto}.preview-text{white-space:pre-wrap;word-break:break-word;font-size:.9em}.preview-html{display:block;width:100%;height:calc(100% - 4px);min-height:60vh;border:none}\n"], dependencies: [{ kind: "component", type: i2$2.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: i2$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i4$3.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "directive", type: MarkdownDirective, selector: "markdown,[markdown]" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
33868
+ }
33869
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileAttachmentPreviewModal, decorators: [{
33870
+ type: Component,
33871
+ args: [{ selector: 'app-file-attachment-preview-modal', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ filename }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"download-button\"\n (click)=\"download()\"\n [title]=\"'FILE.ATTACHMENT.BTN_DOWNLOAD' | translate\"\n >\n <mat-icon slot=\"icon-only\">download</mat-icon>\n </ion-button>\n @if (!mobile && !maximized) {\n <ion-button\n class=\"maximize-button\"\n (click)=\"maximize()\"\n [title]=\"'FILE.ATTACHMENT.BTN_MAXIMIZE' | translate\"\n >\n <mat-icon slot=\"icon-only\">fullscreen</mat-icon>\n </ion-button>\n }\n <ion-button class=\"close-button\" (click)=\"close()\" [title]=\"'COMMON.BTN_CLOSE' | translate\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n @if (loading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else if (error) {\n <ion-text color=\"danger\" class=\"preview-error\">{{ error | translate }}</ion-text>\n } @else if (imageUrl) {\n <img class=\"preview-image\" [src]=\"imageUrl\" [alt]=\"filename\" />\n } @else if (html) {\n <!-- sandboxed: scripts allowed, but no access to the application (no same origin) -->\n <iframe class=\"preview-html\" sandbox=\"allow-scripts\" [srcdoc]=\"html\" [title]=\"filename\"></iframe>\n } @else if (markdown) {\n <markdown class=\"preview-markdown\" [data]=\"text\"></markdown>\n } @else {\n <pre class=\"preview-text\">{{ text }}</pre>\n }\n</ion-content>\n", styles: [".preview-image{display:block;max-width:100%;margin:0 auto}.preview-text{white-space:pre-wrap;word-break:break-word;font-size:.9em}.preview-html{display:block;width:100%;height:calc(100% - 4px);min-height:60vh;border:none}\n"] }]
33872
+ }], propDecorators: { attachment: [{
33873
+ type: Input
33874
+ }] } });
33875
+
33876
+ // GPL v3 License
33877
+ /**
33878
+ * Display an attachment as a compact badge (chip): icon, file name, and size (in the tooltip).
33879
+ *
33880
+ * - Click on the badge: display the file content in a modal (text, markdown, image, HTML), or download it if not previewable;
33881
+ * - Download part (right side, visible on hover - always visible on mobile): download the file.
33882
+ */
33883
+ class FileAttachmentComponent {
33884
+ modalCtrl = inject(ModalController);
33885
+ toastCtrl = inject(ToastController);
33886
+ translate = inject(TranslateService);
33887
+ fileDownload = inject(FileDownloadService);
33888
+ getFilename = FileAttachmentUtils.getFilename;
33889
+ getIcon = FileAttachmentUtils.getIcon;
33890
+ canPreview = (attachment) => this.fileDownload.canPreview({ name: FileAttachmentUtils.getFilename(attachment), mimeType: attachment?.mimeType });
33891
+ attachment;
33892
+ /** Bigger badge (touch), with the download part always visible. Default: the mobile local settings */
33893
+ mobile = inject(LocalSettingsService).mobile;
33894
+ /**
33895
+ * Open the attachment: display its content, if possible (text, markdown, image...), or download it
33896
+ */
33897
+ async open(event) {
33898
+ event?.preventDefault();
33899
+ event?.stopPropagation();
33900
+ const attachment = this.attachment;
33901
+ if (!attachment?.url)
33902
+ return;
33903
+ if (!this.canPreview(attachment)) {
33904
+ return this.download(event);
33905
+ }
33906
+ const hasTopModal = !!(await this.modalCtrl.getTop());
33907
+ const modal = await this.modalCtrl.create({
33908
+ component: FileAttachmentPreviewModal,
33909
+ componentProps: { attachment },
33910
+ cssClass: hasTopModal ? 'stack-modal' : undefined,
33911
+ });
33912
+ await modal.present();
33913
+ }
33914
+ /**
33915
+ * Download the attachment (using the native download manager on Android)
33916
+ */
33917
+ async download(event) {
33918
+ event?.preventDefault();
33919
+ event?.stopPropagation();
33920
+ const attachment = this.attachment;
33921
+ if (!attachment?.url)
33922
+ return;
33923
+ try {
33924
+ await this.fileDownload.download(attachment.url, {
33925
+ filename: FileAttachmentUtils.getFilename(attachment),
33926
+ mimeType: attachment.mimeType,
33927
+ });
33928
+ }
33929
+ catch (err) {
33930
+ console.error(`[file-attachment] Cannot download attachment {${attachment.url}}`, err);
33931
+ await Toasts.show(this.toastCtrl, this.translate, { type: 'error', message: 'FILE.ATTACHMENT.DOWNLOAD_ERROR' });
33932
+ }
33933
+ }
33934
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileAttachmentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
33935
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FileAttachmentComponent, isStandalone: false, selector: "app-file-attachment", inputs: { attachment: "attachment", mobile: "mobile" }, host: { properties: { "class.mobile": "this.mobile" } }, ngImport: i0, template: "@let filename = attachment | map: getFilename;\n@let previewable = attachment | map: canPreview;\n@let action = (previewable ? 'FILE.ATTACHMENT.BTN_PREVIEW' : 'FILE.ATTACHMENT.BTN_DOWNLOAD') | translate;\n<ion-chip\n class=\"file-attachment\"\n [class.has-download]=\"previewable\"\n [outline]=\"true\"\n [title]=\"filename + (attachment?.size ? ' (' + (attachment.size | fileSize) + ')' : '') + ' - ' + action\"\n (click)=\"open($event)\"\n tappable\n>\n <mat-icon class=\"attachment-icon\">{{ attachment | map: getIcon }}</mat-icon>\n <ion-label class=\"attachment-name\">{{ filename }}</ion-label>\n @if (previewable) {\n <!-- download part (separated, visible on hover): preview is the default action -->\n <span class=\"attachment-download\" role=\"button\" [title]=\"'FILE.ATTACHMENT.BTN_DOWNLOAD' | translate\" (click)=\"download($event)\">\n <mat-icon>download</mat-icon>\n </span>\n }\n</ion-chip>\n", styles: [":host{display:inline-flex;max-width:100%;--file-attachment-height: 22px;--file-attachment-icon-size: 14px;--file-attachment-padding: 6px;--file-attachment-font-size: 12px}:host(.mobile){--file-attachment-height: 32px;--file-attachment-icon-size: 18px;--file-attachment-padding: 10px;--file-attachment-font-size: 14px}.file-attachment{max-width:100%;height:var(--file-attachment-height);min-height:var(--file-attachment-height);margin:0;padding:0 var(--file-attachment-padding);font-size:var(--file-attachment-font-size);border-width:1px;overflow:hidden;color:var(--ion-text-color, #000);border-color:rgba(var(--ion-text-color-rgb, 0, 0, 0),.4)}.file-attachment mat-icon{flex-shrink:0;width:var(--file-attachment-icon-size);height:var(--file-attachment-icon-size);font-size:var(--file-attachment-icon-size);line-height:var(--file-attachment-icon-size)}.file-attachment .attachment-icon{margin-inline-end:4px}.file-attachment .attachment-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-attachment .attachment-download{display:flex;align-items:center;align-self:stretch;flex-shrink:0;overflow:hidden;max-width:0;margin-inline-start:0;padding:0;border-inline-start:0 solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.3);opacity:0;transition:max-width .15s ease-in-out,opacity .15s ease-in-out,background-color .15s ease-in-out}.file-attachment.has-download:hover{padding-inline-end:0}.file-attachment:hover .attachment-download{max-width:calc(var(--file-attachment-icon-size) + 2 * var(--file-attachment-padding) + 1px);margin-inline-start:var(--file-attachment-padding);padding:0 var(--file-attachment-padding);border-inline-start-width:1px;opacity:1}.file-attachment .attachment-download:hover{color:var(--ion-color-primary);background-color:rgba(var(--ion-color-primary-rgb, 0, 0, 0),.12)}:host(.mobile) .file-attachment.has-download{padding-inline-end:0}:host(.mobile) .file-attachment .attachment-download{max-width:calc(var(--file-attachment-icon-size) + 2 * var(--file-attachment-padding) + 1px);margin-inline-start:var(--file-attachment-padding);padding:0 var(--file-attachment-padding);border-inline-start-width:1px;opacity:1}\n"], dependencies: [{ kind: "component", type: i2$2.IonChip, selector: "ion-chip", inputs: ["color", "disabled", "mode", "outline"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: FileSizePipe, name: "fileSize" }, { kind: "pipe", type: MapPipe, name: "map" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
33936
+ }
33937
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileAttachmentComponent, decorators: [{
33938
+ type: Component,
33939
+ args: [{ selector: 'app-file-attachment', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "@let filename = attachment | map: getFilename;\n@let previewable = attachment | map: canPreview;\n@let action = (previewable ? 'FILE.ATTACHMENT.BTN_PREVIEW' : 'FILE.ATTACHMENT.BTN_DOWNLOAD') | translate;\n<ion-chip\n class=\"file-attachment\"\n [class.has-download]=\"previewable\"\n [outline]=\"true\"\n [title]=\"filename + (attachment?.size ? ' (' + (attachment.size | fileSize) + ')' : '') + ' - ' + action\"\n (click)=\"open($event)\"\n tappable\n>\n <mat-icon class=\"attachment-icon\">{{ attachment | map: getIcon }}</mat-icon>\n <ion-label class=\"attachment-name\">{{ filename }}</ion-label>\n @if (previewable) {\n <!-- download part (separated, visible on hover): preview is the default action -->\n <span class=\"attachment-download\" role=\"button\" [title]=\"'FILE.ATTACHMENT.BTN_DOWNLOAD' | translate\" (click)=\"download($event)\">\n <mat-icon>download</mat-icon>\n </span>\n }\n</ion-chip>\n", styles: [":host{display:inline-flex;max-width:100%;--file-attachment-height: 22px;--file-attachment-icon-size: 14px;--file-attachment-padding: 6px;--file-attachment-font-size: 12px}:host(.mobile){--file-attachment-height: 32px;--file-attachment-icon-size: 18px;--file-attachment-padding: 10px;--file-attachment-font-size: 14px}.file-attachment{max-width:100%;height:var(--file-attachment-height);min-height:var(--file-attachment-height);margin:0;padding:0 var(--file-attachment-padding);font-size:var(--file-attachment-font-size);border-width:1px;overflow:hidden;color:var(--ion-text-color, #000);border-color:rgba(var(--ion-text-color-rgb, 0, 0, 0),.4)}.file-attachment mat-icon{flex-shrink:0;width:var(--file-attachment-icon-size);height:var(--file-attachment-icon-size);font-size:var(--file-attachment-icon-size);line-height:var(--file-attachment-icon-size)}.file-attachment .attachment-icon{margin-inline-end:4px}.file-attachment .attachment-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-attachment .attachment-download{display:flex;align-items:center;align-self:stretch;flex-shrink:0;overflow:hidden;max-width:0;margin-inline-start:0;padding:0;border-inline-start:0 solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.3);opacity:0;transition:max-width .15s ease-in-out,opacity .15s ease-in-out,background-color .15s ease-in-out}.file-attachment.has-download:hover{padding-inline-end:0}.file-attachment:hover .attachment-download{max-width:calc(var(--file-attachment-icon-size) + 2 * var(--file-attachment-padding) + 1px);margin-inline-start:var(--file-attachment-padding);padding:0 var(--file-attachment-padding);border-inline-start-width:1px;opacity:1}.file-attachment .attachment-download:hover{color:var(--ion-color-primary);background-color:rgba(var(--ion-color-primary-rgb, 0, 0, 0),.12)}:host(.mobile) .file-attachment.has-download{padding-inline-end:0}:host(.mobile) .file-attachment .attachment-download{max-width:calc(var(--file-attachment-icon-size) + 2 * var(--file-attachment-padding) + 1px);margin-inline-start:var(--file-attachment-padding);padding:0 var(--file-attachment-padding);border-inline-start-width:1px;opacity:1}\n"] }]
33940
+ }], propDecorators: { attachment: [{
33941
+ type: Input,
33942
+ args: [{ required: true }]
33943
+ }], mobile: [{
33944
+ type: HostBinding,
33945
+ args: ['class.mobile']
33946
+ }, {
33947
+ type: Input
33948
+ }] } });
33949
+
33932
33950
  const PRINT_LOADING_STORAGE_KEY_PREFIX = 'print-loading-';
33933
33951
  const PRINT_ID_QUERY_PARAM = 'print-id';
33934
33952
  class PrintService {
@@ -34223,6 +34241,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
34223
34241
  args: [DOCUMENT]
34224
34242
  }] }] });
34225
34243
 
34244
+ class DebugComponent {
34245
+ titlePrefix = 'COMMON.DEBUG.TITLE';
34246
+ title = '';
34247
+ enable = null;
34248
+ expanded = false;
34249
+ expansionPanel;
34250
+ constructor(environment) {
34251
+ this.enable = !environment || !environment.production;
34252
+ }
34253
+ toggle() {
34254
+ this.expansionPanel.toggle();
34255
+ }
34256
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DebugComponent, deps: [{ token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Component });
34257
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.22", type: DebugComponent, isStandalone: false, selector: "app-debug", inputs: { titlePrefix: "titlePrefix", title: "title", enable: "enable", expanded: "expanded" }, viewQueries: [{ propertyName: "expansionPanel", first: true, predicate: ["expansionPanel"], descendants: true }], ngImport: i0, template: "<!-- debug -->\n<ion-item tappable (click)=\"toggle()\" lines=\"none\" color=\"secondary50\">\n <ion-icon slot=\"start\" name=\"bug\"></ion-icon>\n <ion-label color=\"primary\">{{ titlePrefix | translate }} {{ title }}</ion-label>\n <mat-icon slot=\"end\">{{ expansionPanel.expanded ? 'expand_less' : 'expand_more' }}</mat-icon>\n</ion-item>\n<mat-expansion-panel #expansionPanel [(expanded)]=\"expanded\">\n <ion-text color=\"primary100\">\n <div class=\"ion-margin-start\">\n <ng-content></ng-content>\n </div>\n </ion-text>\n</mat-expansion-panel>\n", styles: ["ion-item mat-icon,ion-item ion-icon{color:var(--ion-color-primary)}ion-item{cursor:pointer!important}.mat-expansion-panel{--expansion-panel-padding: 4px;background:var(--ion-color-secondary50)!important}.mat-expansion-panel.mat-expanded{margin-bottom:4px!important}.mat-expansion-panel .mat-expansion-panel-body *{color:var(--ion-color-primary100)}\n"], dependencies: [{ kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i11$2.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }] });
34258
+ }
34259
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: DebugComponent, decorators: [{
34260
+ type: Component,
34261
+ args: [{ selector: 'app-debug', standalone: false, template: "<!-- debug -->\n<ion-item tappable (click)=\"toggle()\" lines=\"none\" color=\"secondary50\">\n <ion-icon slot=\"start\" name=\"bug\"></ion-icon>\n <ion-label color=\"primary\">{{ titlePrefix | translate }} {{ title }}</ion-label>\n <mat-icon slot=\"end\">{{ expansionPanel.expanded ? 'expand_less' : 'expand_more' }}</mat-icon>\n</ion-item>\n<mat-expansion-panel #expansionPanel [(expanded)]=\"expanded\">\n <ion-text color=\"primary100\">\n <div class=\"ion-margin-start\">\n <ng-content></ng-content>\n </div>\n </ion-text>\n</mat-expansion-panel>\n", styles: ["ion-item mat-icon,ion-item ion-icon{color:var(--ion-color-primary)}ion-item{cursor:pointer!important}.mat-expansion-panel{--expansion-panel-padding: 4px;background:var(--ion-color-secondary50)!important}.mat-expansion-panel.mat-expanded{margin-bottom:4px!important}.mat-expansion-panel .mat-expansion-panel-body *{color:var(--ion-color-primary100)}\n"] }]
34262
+ }], ctorParameters: () => [{ type: Environment, decorators: [{
34263
+ type: Inject,
34264
+ args: [ENVIRONMENT]
34265
+ }, {
34266
+ type: Optional
34267
+ }] }], propDecorators: { titlePrefix: [{
34268
+ type: Input
34269
+ }], title: [{
34270
+ type: Input
34271
+ }], enable: [{
34272
+ type: Input
34273
+ }], expanded: [{
34274
+ type: Input
34275
+ }], expansionPanel: [{
34276
+ type: ViewChild,
34277
+ args: ['expansionPanel']
34278
+ }] } });
34279
+
34226
34280
  class AppMarkdownContent {
34227
34281
  cd;
34228
34282
  element;
@@ -34612,6 +34666,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
34612
34666
  args: ['content']
34613
34667
  }] } });
34614
34668
 
34669
+ class SharedDebugModule {
34670
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedDebugModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
34671
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: SharedDebugModule, declarations: [DebugComponent], imports: [CommonModule, SharedIonicModule, MatExpansionModule, MatIconModule, TranslatePipe], exports: [DebugComponent] });
34672
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedDebugModule, providers: [provideChildTranslateService()], imports: [CommonModule, SharedIonicModule, MatExpansionModule, MatIconModule] });
34673
+ }
34674
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedDebugModule, decorators: [{
34675
+ type: NgModule,
34676
+ args: [{
34677
+ imports: [CommonModule, SharedIonicModule, MatExpansionModule, MatIconModule, TranslatePipe],
34678
+ declarations: [DebugComponent],
34679
+ exports: [DebugComponent],
34680
+ providers: [provideChildTranslateService()],
34681
+ }]
34682
+ }] });
34683
+
34615
34684
  // function that returns `MarkedOptions` with renderer override
34616
34685
  class SharedMarkdownModule {
34617
34686
  static forRoot() {
@@ -34664,29 +34733,258 @@ class SharedMarkdownModule {
34664
34733
  SharedToolbarModule,
34665
34734
  SharedDebugModule, MarkdownModule] });
34666
34735
  }
34667
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedMarkdownModule, decorators: [{
34668
- type: NgModule,
34669
- args: [{
34670
- imports: [
34671
- CommonModule,
34672
- SharedIonicModule,
34673
- TranslatePipe,
34674
- MarkdownModule.forChild(),
34675
- // App module
34676
- SharedToolbarModule,
34677
- SharedDebugModule,
34678
- ],
34679
- declarations: [
34680
- // Component
34681
- AppMarkdownContent,
34682
- AppMarkdownModal,
34683
- // Directive
34684
- MarkdownDirective,
34685
- ],
34686
- exports: [MarkdownModule, AppMarkdownContent, MarkdownDirective],
34687
- providers: [provideChildTranslateService()],
34688
- }]
34689
- }] });
34736
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedMarkdownModule, decorators: [{
34737
+ type: NgModule,
34738
+ args: [{
34739
+ imports: [
34740
+ CommonModule,
34741
+ SharedIonicModule,
34742
+ TranslatePipe,
34743
+ MarkdownModule.forChild(),
34744
+ // App module
34745
+ SharedToolbarModule,
34746
+ SharedDebugModule,
34747
+ ],
34748
+ declarations: [
34749
+ // Component
34750
+ AppMarkdownContent,
34751
+ AppMarkdownModal,
34752
+ // Directive
34753
+ MarkdownDirective,
34754
+ ],
34755
+ exports: [MarkdownModule, AppMarkdownContent, MarkdownDirective],
34756
+ providers: [provideChildTranslateService()],
34757
+ }]
34758
+ }] });
34759
+
34760
+ // GPL v3 License
34761
+ class SharedFileAttachmentModule {
34762
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedFileAttachmentModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
34763
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: SharedFileAttachmentModule, declarations: [FileAttachmentComponent, FileAttachmentPreviewModal], imports: [CommonModule, SharedIonicModule, SharedMaterialModule, SharedPipesModule, SharedMarkdownModule, TranslatePipe], exports: [FileAttachmentComponent, FileAttachmentPreviewModal] });
34764
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedFileAttachmentModule, providers: [provideChildTranslateService()], imports: [CommonModule, SharedIonicModule, SharedMaterialModule, SharedPipesModule, SharedMarkdownModule] });
34765
+ }
34766
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedFileAttachmentModule, decorators: [{
34767
+ type: NgModule,
34768
+ args: [{
34769
+ imports: [CommonModule, SharedIonicModule, SharedMaterialModule, SharedPipesModule, SharedMarkdownModule, TranslatePipe],
34770
+ declarations: [FileAttachmentComponent, FileAttachmentPreviewModal],
34771
+ exports: [FileAttachmentComponent, FileAttachmentPreviewModal],
34772
+ providers: [provideChildTranslateService()],
34773
+ }]
34774
+ }] });
34775
+
34776
+ /**
34777
+ * Define here theme colors
34778
+ */
34779
+ const rgbArrayMap = {
34780
+ white: [255, 255, 255],
34781
+ primary: [20, 67, 145], // ok
34782
+ secondary: [117, 196, 253], // ok
34783
+ tertiary: [91, 94, 244], // ok
34784
+ danger: [245, 61, 61], // ok
34785
+ light: [244, 245, 248], // ok
34786
+ medium: [152, 154, 162], // ok
34787
+ dark: [34, 36, 40], // ok
34788
+ red: [255, 0, 0],
34789
+ green: [0, 255, 0],
34790
+ blue: [0, 0, 255],
34791
+ };
34792
+ // Fill a map of Color objects
34793
+ const colorsMap = {};
34794
+ /**
34795
+ * Useful class for color conversion
34796
+ */
34797
+ // @dynamic
34798
+ class Color {
34799
+ _rgbArray;
34800
+ _opacity;
34801
+ _name;
34802
+ // Helper method, to retrieve a color
34803
+ static get(name) {
34804
+ return colorsMap[name];
34805
+ }
34806
+ static parseRgba(rgba) {
34807
+ if (!rgba || (!rgba.startsWith('rgb(') && !rgba.startsWith('rgba(')))
34808
+ return null;
34809
+ // Parse parts
34810
+ const parts = rgba.replace('rgb(', '').replace('rgba(', '').replace(')', '').split(',');
34811
+ if (parts.length !== 3 && parts.length !== 4)
34812
+ return null;
34813
+ return new Color([+parts[0], +parts[1], +parts[2]], (parts.length === 4 && +parts[3]) || 1, 'custom');
34814
+ }
34815
+ static transparent = function () {
34816
+ return new Color([0, 0, 0], 0, 'translucent');
34817
+ };
34818
+ constructor(_rgbArray, _opacity, _name = 'custom') {
34819
+ this._rgbArray = _rgbArray;
34820
+ this._opacity = _opacity;
34821
+ this._name = _name;
34822
+ }
34823
+ get name() {
34824
+ return this._name;
34825
+ }
34826
+ get opacity() {
34827
+ return isNotNil(this._opacity) ? this._opacity : 1;
34828
+ }
34829
+ get rgb() {
34830
+ return this._rgbArray;
34831
+ }
34832
+ get r() {
34833
+ return this._rgbArray[0];
34834
+ }
34835
+ get g() {
34836
+ return this._rgbArray[1];
34837
+ }
34838
+ get b() {
34839
+ return this._rgbArray[2];
34840
+ }
34841
+ rgba(opacity) {
34842
+ opacity = isNotNil(opacity) ? opacity : this._opacity;
34843
+ if (isNil(opacity) || opacity < 0 || opacity > 1) {
34844
+ return 'rgb(' + this._rgbArray.join(',') + ')';
34845
+ }
34846
+ return 'rgba(' + this._rgbArray.join(',') + ',' + opacity + ')';
34847
+ }
34848
+ }
34849
+ /**
34850
+ * Helper class for colors scale
34851
+ */
34852
+ // @dynamic
34853
+ class ColorScale {
34854
+ static custom = (count, options) => {
34855
+ options = options || {};
34856
+ return new ColorScale(linearColorGradientWithIntermediate(count, {
34857
+ startColor: options.startColor || undefined,
34858
+ mainColor: options.mainColor || undefined,
34859
+ mainColorIndex: isNotNil(options.mainColorIndex) ? options.mainColorIndex : undefined,
34860
+ endColor: options.endColor || undefined,
34861
+ }), options);
34862
+ };
34863
+ static default() {
34864
+ return ColorScale.custom(25);
34865
+ }
34866
+ /**
34867
+ * Create an array with the given color
34868
+ **/
34869
+ static fix(length, colorName) {
34870
+ const color = Color.get(colorName || 'primary').rgba(0.5);
34871
+ return new Array(length || 25).fill(color);
34872
+ }
34873
+ _min;
34874
+ _max;
34875
+ _rangeSize;
34876
+ _legendItems;
34877
+ get min() {
34878
+ return this._min;
34879
+ }
34880
+ get max() {
34881
+ return this._max;
34882
+ }
34883
+ constructor(colorArray, options) {
34884
+ options = options || {};
34885
+ // reserved last colors for value > max
34886
+ const nbIntervalBeforeUpper = !options.upperMax ? colorArray.length : colorArray.length - 1;
34887
+ this._min = options.min || 0;
34888
+ this._max = options.max || nbIntervalBeforeUpper;
34889
+ this._rangeSize = Math.round((this._max - this._min) / nbIntervalBeforeUpper);
34890
+ this._legendItems = this.computeLegendItems(colorArray);
34891
+ }
34892
+ get legend() {
34893
+ return {
34894
+ items: this._legendItems,
34895
+ };
34896
+ }
34897
+ getValueColor(value) {
34898
+ return this.getLegendForValue(value).color;
34899
+ }
34900
+ getLegendForValue(value) {
34901
+ if (value <= this._min)
34902
+ return this._legendItems[0];
34903
+ if (value >= this._max)
34904
+ return this._legendItems[this._legendItems.length - 1];
34905
+ const index = Math.floor((value * (this._legendItems.length - 1)) / this._max);
34906
+ return this._legendItems[index];
34907
+ }
34908
+ getLegendAtIndex(index) {
34909
+ return this._legendItems[index];
34910
+ }
34911
+ computeLegendItems(colorArray) {
34912
+ return colorArray.map((color, index) => {
34913
+ const start = index * this._rangeSize;
34914
+ const end = start + this._rangeSize;
34915
+ return {
34916
+ color: new Color(color),
34917
+ label: end < this._max ? `${start.toLocaleString()} - ${end.toLocaleString()}` : ` >= ${start}`,
34918
+ };
34919
+ });
34920
+ }
34921
+ }
34922
+ // Fill colorsMap
34923
+ Object.getOwnPropertyNames(rgbArrayMap).forEach((key) => {
34924
+ colorsMap[key] = new Color(rgbArrayMap[key], 1, key);
34925
+ });
34926
+ /**
34927
+ * Internal function, that create a colors scale, using iteration
34928
+ *
34929
+ * @param count
34930
+ * @param opacity
34931
+ * @param startColor
34932
+ * @param startState
34933
+ * @returns
34934
+ */
34935
+ function linearColorGradientWithIntermediate(count, options) {
34936
+ options = options || {};
34937
+ // From [0,1]
34938
+ options.startColor = options.startColor || [255, 255, 190]; // default start = creme
34939
+ options.mainColorIndex = options.mainColorIndex && options.mainColorIndex < count - 1 ? options.mainColorIndex : count - 1;
34940
+ options.endColor = options.endColor || [255, 0, 0]; // default main = red
34941
+ if (!options.mainColor) {
34942
+ return linearColorGradient(count, {
34943
+ startColor: options.startColor,
34944
+ endColor: options.endColor,
34945
+ });
34946
+ }
34947
+ else {
34948
+ // Step 1: startColor -> mainColor
34949
+ const result = linearColorGradient(options.mainColorIndex + 1, {
34950
+ startColor: options.startColor,
34951
+ endColor: options.mainColor,
34952
+ });
34953
+ // Step 2: mainColor -> endColor
34954
+ if (options.mainColorIndex < count - 1) {
34955
+ return result.concat(linearColorGradient(count - options.mainColorIndex, {
34956
+ startColor: options.mainColor,
34957
+ endColor: options.endColor,
34958
+ }));
34959
+ }
34960
+ else {
34961
+ return result;
34962
+ }
34963
+ }
34964
+ }
34965
+ function linearColorGradient(count, options) {
34966
+ options = options || {};
34967
+ // From [0,1]
34968
+ options.startColor = options.startColor || [255, 255, 255]; // default start = white
34969
+ options.endColor = options.endColor || [255, 0, 0]; // default end = red
34970
+ const result = [];
34971
+ const color = options.startColor.slice(); // copy the start color
34972
+ const delta = [
34973
+ Math.round((options.endColor[0] - options.startColor[0]) / count),
34974
+ Math.round((options.endColor[1] - options.startColor[1]) / count),
34975
+ Math.round((options.endColor[2] - options.startColor[2]) / count),
34976
+ ];
34977
+ for (let i = 0; i < count - 1; i++) {
34978
+ for (let j = 0; j < 3; j++) {
34979
+ color[j] += delta[j];
34980
+ }
34981
+ result.push(color.slice());
34982
+ }
34983
+ // Force last color = end color
34984
+ result.push(options.endColor.slice());
34985
+ // Output as array
34986
+ return result;
34987
+ }
34690
34988
 
34691
34989
  const APP_ABOUT_DEVELOPERS = new InjectionToken('aboutDevelopers');
34692
34990
  const APP_ABOUT_PARTNERS = new InjectionToken('aboutPartners');
@@ -39819,120 +40117,6 @@ function providePersonService() {
39819
40117
  };
39820
40118
  }
39821
40119
 
39822
- // GPL v3 License
39823
- /** Kinds of file that can be previewed (displayed without being downloaded) */
39824
- const PREVIEW_KINDS = Object.freeze(['image', 'text', 'code']);
39825
- /** Extra MIME types (or extensions) that can be previewed as text */
39826
- const PREVIEW_TEXT_MIME_TYPES = /^(text\/(csv|tab-separated-values|markdown|x-markdown)|application\/csv)$/;
39827
- const PREVIEW_TEXT_EXTENSIONS = Object.freeze(['csv', 'tsv', 'md', 'markdown']);
39828
- const MARKDOWN_MIME_TYPES = /^text\/(x-)?markdown$/;
39829
- const MARKDOWN_EXTENSIONS = Object.freeze(['md', 'markdown']);
39830
- const HTML_MIME_TYPES = /^(text\/html|application\/xhtml\+xml)$/;
39831
- const HTML_EXTENSIONS = Object.freeze(['html', 'htm', 'xhtml']);
39832
- /**
39833
- * Download (or read) a remote file, sending the application headers (e.g. authorization) when the file is on the current peer.
39834
- *
39835
- * - On Android (Cordova downloader): the file is downloaded by the native download manager (visible in the "Downloads" app);
39836
- * - Otherwise (web, iOS): the file is fetched as a Blob, then saved using a temporary object URL.
39837
- */
39838
- class FileDownloadService {
39839
- network = inject(NetworkService);
39840
- platform = inject(PlatformService);
39841
- /**
39842
- * Can the file be previewed (displayed without being downloaded)?
39843
- */
39844
- canPreview(file) {
39845
- return this.isImage(file) || this.isText(file);
39846
- }
39847
- isImage(file) {
39848
- return FileTypeUtils.getKindByFile({ name: file?.name, type: file?.mimeType }) === 'image';
39849
- }
39850
- isText(file) {
39851
- const mimeType = file?.mimeType?.trim().toLowerCase();
39852
- if (mimeType && PREVIEW_TEXT_MIME_TYPES.test(mimeType))
39853
- return true;
39854
- if (PREVIEW_TEXT_EXTENSIONS.includes(FileTypeUtils.getExtension(file?.name)))
39855
- return true;
39856
- const kind = FileTypeUtils.getKindByFile({ name: file?.name, type: mimeType });
39857
- return kind !== 'image' && PREVIEW_KINDS.includes(kind);
39858
- }
39859
- isMarkdown(file) {
39860
- const mimeType = file?.mimeType?.trim().toLowerCase();
39861
- return (mimeType && MARKDOWN_MIME_TYPES.test(mimeType)) || MARKDOWN_EXTENSIONS.includes(FileTypeUtils.getExtension(file?.name));
39862
- }
39863
- isHtml(file) {
39864
- const mimeType = file?.mimeType?.trim().toLowerCase();
39865
- return (mimeType && HTML_MIME_TYPES.test(mimeType)) || HTML_EXTENSIONS.includes(FileTypeUtils.getExtension(file?.name));
39866
- }
39867
- /**
39868
- * Fetch a remote file, as a Blob
39869
- */
39870
- getBlob(url) {
39871
- return this.network.getBlob(FileDownloadService.normalizeUrl(url));
39872
- }
39873
- /**
39874
- * Remove duplicated slashes in the path of an URL (e.g. 'http://host//api/file' => 'http://host/api/file').
39875
- * Needed because the server (and its CORS rules) does not match a path with duplicated slashes.
39876
- */
39877
- static normalizeUrl(url) {
39878
- if (!url)
39879
- return url;
39880
- const [path, ...query] = url.split('?');
39881
- const normalizedPath = path.replace(/([^:\/])\/{2,}/g, '$1/');
39882
- return [normalizedPath, ...query].join('?');
39883
- }
39884
- /**
39885
- * Fetch a remote file, as a text (e.g. to preview it)
39886
- */
39887
- async getText(url) {
39888
- const blob = await this.getBlob(url);
39889
- return blob.text();
39890
- }
39891
- /**
39892
- * Download a remote file
39893
- */
39894
- async download(url, opts) {
39895
- if (!url)
39896
- throw new Error("Missing argument 'url'");
39897
- url = FileDownloadService.normalizeUrl(url);
39898
- const filename = opts?.filename || this.getFilename(url);
39899
- // Android: use the native download manager
39900
- if (this.platform.canDownload) {
39901
- const headers = Object.entries(this.network.getAppHeaders(url)).map(([header, value]) => ({
39902
- header,
39903
- value: Array.isArray(value) ? value.join(', ') : value,
39904
- }));
39905
- await firstValueFrom(this.platform.download({ uri: url, filename, title: filename, mimeType: opts?.mimeType, headers }));
39906
- return;
39907
- }
39908
- // Web, iOS: fetch as a Blob (with the authorization headers), then save it
39909
- const blob = await this.getBlob(url);
39910
- const objectUrl = URL.createObjectURL(blob);
39911
- try {
39912
- FilesUtils.downloadUri(objectUrl, filename);
39913
- }
39914
- finally {
39915
- // Release the object URL, once the download has started
39916
- setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
39917
- }
39918
- }
39919
- /* -- protected methods -- */
39920
- getFilename(url) {
39921
- try {
39922
- return FilesUtils.getFilenameFromUri(url.split('?')[0]) || 'download';
39923
- }
39924
- catch {
39925
- return 'download';
39926
- }
39927
- }
39928
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileDownloadService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
39929
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileDownloadService, providedIn: 'root' });
39930
- }
39931
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FileDownloadService, decorators: [{
39932
- type: Injectable,
39933
- args: [{ providedIn: 'root' }]
39934
- }] });
39935
-
39936
40120
  // GPL v3 License
39937
40121
  const APP_FILE_TRANSFER_SERVICE = new InjectionToken('FileTransferService');
39938
40122
 
@@ -40406,97 +40590,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
40406
40590
  }] }] });
40407
40591
 
40408
40592
  class FeedAttachmentUtils {
40593
+ /**
40594
+ * Convert a feed attachment (JSON Feed format) into a generic file attachment
40595
+ */
40596
+ static toFileAttachment(attachment) {
40597
+ if (!attachment)
40598
+ return undefined;
40599
+ return {
40600
+ url: attachment.url,
40601
+ name: attachment.title?.trim() || undefined,
40602
+ mimeType: attachment.mime_type,
40603
+ size: attachment.size_in_bytes,
40604
+ };
40605
+ }
40409
40606
  /**
40410
40607
  * Get the file name of an attachment (its title, or the last part of its URL)
40411
40608
  */
40412
40609
  static getFilename(attachment) {
40413
- if (attachment?.title?.trim())
40414
- return attachment.title.trim();
40415
- const path = attachment?.url?.split('?')[0];
40416
- return path?.substring(path.lastIndexOf('/') + 1) || 'attachment';
40610
+ return FileAttachmentUtils.getFilename(FeedAttachmentUtils.toFileAttachment(attachment));
40417
40611
  }
40418
40612
  /**
40419
40613
  * Get the Material icon (font ligature) of an attachment
40420
40614
  */
40421
40615
  static getIcon(attachment) {
40422
- return FileTypeUtils.getIconByFile({ name: attachment?.title, type: attachment?.mime_type });
40423
- }
40424
- }
40425
-
40426
- // GPL v3 License
40427
- /**
40428
- * Display the content of a feed attachment (text, markdown, image), without downloading it
40429
- */
40430
- class FeedAttachmentPreviewModal {
40431
- cd = inject(ChangeDetectorRef);
40432
- modalCtrl = inject(ModalController);
40433
- fileDownload = inject(FileDownloadService);
40434
- mobile = inject(LocalSettingsService).mobile;
40435
- sanitizer = inject(DomSanitizer);
40436
- elementRef = inject(ElementRef);
40437
- attachment;
40438
- loading = true;
40439
- error;
40440
- filename;
40441
- text;
40442
- markdown = false;
40443
- imageUrl;
40444
- /** HTML content, displayed in a sandboxed iframe (scripts allowed, but isolated from the application: no same origin) */
40445
- html;
40446
- /** Display the modal in a large size (same as the feed details modal) */
40447
- maximized = false;
40448
- async ngOnInit() {
40449
- this.filename = FeedAttachmentUtils.getFilename(this.attachment);
40450
- const file = { name: this.filename, mimeType: this.attachment?.mime_type };
40451
- try {
40452
- if (this.fileDownload.isImage(file)) {
40453
- const blob = await this.fileDownload.getBlob(this.attachment.url);
40454
- this.imageUrl = URL.createObjectURL(blob);
40455
- }
40456
- else if (this.fileDownload.isHtml(file)) {
40457
- const content = await this.fileDownload.getText(this.attachment.url);
40458
- // Safe: the content is only rendered inside a sandboxed iframe (see template)
40459
- this.html = this.sanitizer.bypassSecurityTrustHtml(content);
40460
- }
40461
- else {
40462
- this.markdown = this.fileDownload.isMarkdown(file);
40463
- this.text = await this.fileDownload.getText(this.attachment.url);
40464
- }
40465
- }
40466
- catch (err) {
40467
- console.error(`[feed-attachment] Cannot load attachment {${this.attachment?.url}}`, err);
40468
- this.error = 'SOCIAL.FEED.ATTACHMENTS.PREVIEW_ERROR';
40469
- }
40470
- finally {
40471
- this.loading = false;
40472
- this.cd.markForCheck();
40473
- }
40474
- }
40475
- ngOnDestroy() {
40476
- if (this.imageUrl)
40477
- URL.revokeObjectURL(this.imageUrl);
40478
- }
40479
- async download() {
40480
- await this.fileDownload.download(this.attachment.url, { filename: this.filename, mimeType: this.attachment.mime_type });
40481
- }
40482
- /** Display the modal in a large size (to restore the size: close then reopen the attachment) */
40483
- maximize() {
40484
- this.maximized = true;
40485
- this.elementRef.nativeElement.closest('ion-modal')?.classList.add('modal-large');
40486
- this.cd.markForCheck();
40487
- }
40488
- close() {
40489
- return this.modalCtrl.dismiss();
40616
+ return FileAttachmentUtils.getIcon(FeedAttachmentUtils.toFileAttachment(attachment));
40490
40617
  }
40491
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FeedAttachmentPreviewModal, deps: [], target: i0.ɵɵFactoryTarget.Component });
40492
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FeedAttachmentPreviewModal, isStandalone: false, selector: "app-feed-attachment-preview-modal", inputs: { attachment: "attachment" }, ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ filename }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"download-button\"\n (click)=\"download()\"\n [title]=\"'SOCIAL.FEED.ATTACHMENTS.BTN_DOWNLOAD' | translate\"\n >\n <mat-icon slot=\"icon-only\">download</mat-icon>\n </ion-button>\n @if (!mobile && !maximized) {\n <ion-button\n class=\"maximize-button\"\n (click)=\"maximize()\"\n [title]=\"'SOCIAL.FEED.ATTACHMENTS.BTN_MAXIMIZE' | translate\"\n >\n <mat-icon slot=\"icon-only\">fullscreen</mat-icon>\n </ion-button>\n }\n <ion-button class=\"close-button\" (click)=\"close()\" [title]=\"'COMMON.BTN_CLOSE' | translate\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n @if (loading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else if (error) {\n <ion-text color=\"danger\" class=\"preview-error\">{{ error | translate }}</ion-text>\n } @else if (imageUrl) {\n <img class=\"preview-image\" [src]=\"imageUrl\" [alt]=\"filename\" />\n } @else if (html) {\n <!-- sandboxed: scripts allowed, but no access to the application (no same origin) -->\n <iframe class=\"preview-html\" sandbox=\"allow-scripts\" [srcdoc]=\"html\" [title]=\"filename\"></iframe>\n } @else if (markdown) {\n <markdown class=\"preview-markdown\" [data]=\"text\"></markdown>\n } @else {\n <pre class=\"preview-text\">{{ text }}</pre>\n }\n</ion-content>\n", styles: [".preview-image{display:block;max-width:100%;margin:0 auto}.preview-text{white-space:pre-wrap;word-break:break-word;font-size:.9em}.preview-html{display:block;width:100%;height:calc(100% - 4px);min-height:60vh;border:none}\n"], dependencies: [{ kind: "component", type: i2$2.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: i2$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonSpinner, selector: "ion-spinner", inputs: ["color", "duration", "name", "paused"] }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i4$3.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MarkdownDirective, selector: "markdown,[markdown]" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
40493
40618
  }
40494
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FeedAttachmentPreviewModal, decorators: [{
40495
- type: Component,
40496
- args: [{ selector: 'app-feed-attachment-preview-modal', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ filename }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"download-button\"\n (click)=\"download()\"\n [title]=\"'SOCIAL.FEED.ATTACHMENTS.BTN_DOWNLOAD' | translate\"\n >\n <mat-icon slot=\"icon-only\">download</mat-icon>\n </ion-button>\n @if (!mobile && !maximized) {\n <ion-button\n class=\"maximize-button\"\n (click)=\"maximize()\"\n [title]=\"'SOCIAL.FEED.ATTACHMENTS.BTN_MAXIMIZE' | translate\"\n >\n <mat-icon slot=\"icon-only\">fullscreen</mat-icon>\n </ion-button>\n }\n <ion-button class=\"close-button\" (click)=\"close()\" [title]=\"'COMMON.BTN_CLOSE' | translate\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n @if (loading) {\n <ion-spinner name=\"crescent\"></ion-spinner>\n } @else if (error) {\n <ion-text color=\"danger\" class=\"preview-error\">{{ error | translate }}</ion-text>\n } @else if (imageUrl) {\n <img class=\"preview-image\" [src]=\"imageUrl\" [alt]=\"filename\" />\n } @else if (html) {\n <!-- sandboxed: scripts allowed, but no access to the application (no same origin) -->\n <iframe class=\"preview-html\" sandbox=\"allow-scripts\" [srcdoc]=\"html\" [title]=\"filename\"></iframe>\n } @else if (markdown) {\n <markdown class=\"preview-markdown\" [data]=\"text\"></markdown>\n } @else {\n <pre class=\"preview-text\">{{ text }}</pre>\n }\n</ion-content>\n", styles: [".preview-image{display:block;max-width:100%;margin:0 auto}.preview-text{white-space:pre-wrap;word-break:break-word;font-size:.9em}.preview-html{display:block;width:100%;height:calc(100% - 4px);min-height:60vh;border:none}\n"] }]
40497
- }], propDecorators: { attachment: [{
40498
- type: Input
40499
- }] } });
40500
40619
 
40501
40620
  class FeedDirective {
40502
40621
  element;
@@ -40650,9 +40769,6 @@ class FeedsComponent {
40650
40769
  platform = inject(PlatformService);
40651
40770
  networkService = inject(NetworkService);
40652
40771
  router = inject(Router);
40653
- fileDownload = inject(FileDownloadService);
40654
- modalCtrl = inject(ModalController);
40655
- toastCtrl = inject(ToastController);
40656
40772
  version;
40657
40773
  modalItemId;
40658
40774
  onRefresh = new EventEmitter();
@@ -40876,47 +40992,7 @@ class FeedsComponent {
40876
40992
  getVisibleEntries(feeds, filterItem) {
40877
40993
  return (feeds || []).flatMap((feed) => (feed?.items || []).filter((item) => !filterItem || filterItem(item)).map((item) => ({ feed, item })));
40878
40994
  }
40879
- getAttachmentFilename = FeedAttachmentUtils.getFilename;
40880
- getAttachmentIcon = FeedAttachmentUtils.getIcon;
40881
- canPreviewAttachment = (attachment) => this.fileDownload.canPreview({ name: FeedAttachmentUtils.getFilename(attachment), mimeType: attachment?.mime_type });
40882
- /**
40883
- * Open an attachment: display its content, if possible (text, markdown, image...), or download it
40884
- */
40885
- async openAttachment(event, attachment) {
40886
- event?.preventDefault();
40887
- event?.stopPropagation();
40888
- if (!attachment?.url)
40889
- return;
40890
- if (!this.canPreviewAttachment(attachment)) {
40891
- return this.downloadAttachment(event, attachment);
40892
- }
40893
- const hasTopModal = !!(await this.modalCtrl.getTop());
40894
- const modal = await this.modalCtrl.create({
40895
- component: FeedAttachmentPreviewModal,
40896
- componentProps: { attachment },
40897
- cssClass: hasTopModal ? 'stack-modal' : undefined,
40898
- });
40899
- await modal.present();
40900
- }
40901
- /**
40902
- * Download an attachment (using the native download manager on Android)
40903
- */
40904
- async downloadAttachment(event, attachment) {
40905
- event?.preventDefault();
40906
- event?.stopPropagation();
40907
- if (!attachment?.url)
40908
- return;
40909
- try {
40910
- await this.fileDownload.download(attachment.url, {
40911
- filename: FeedAttachmentUtils.getFilename(attachment),
40912
- mimeType: attachment.mime_type,
40913
- });
40914
- }
40915
- catch (err) {
40916
- console.error(`[feed-component] Cannot download attachment {${attachment.url}}`, err);
40917
- await Toasts.show(this.toastCtrl, this.translate, { type: 'error', message: 'SOCIAL.FEED.ATTACHMENTS.DOWNLOAD_ERROR' });
40918
- }
40919
- }
40995
+ toFileAttachment = FeedAttachmentUtils.toFileAttachment;
40920
40996
  getFeedHomeUrl(feed) {
40921
40997
  return feed.home_page_url ?? JsonFeedUtils.removeJsonExtension(feed.feed_url) ?? this.feedService.getHomeUrl();
40922
40998
  }
@@ -40982,11 +41058,11 @@ class FeedsComponent {
40982
41058
  }
40983
41059
  }
40984
41060
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FeedsComponent, deps: [{ token: LocalSettingsService }, { token: ENVIRONMENT }, { token: APP_FEED_SERVICE, optional: true }, { token: AccountService }, { token: i2$2.AlertController }], target: i0.ɵɵFactoryTarget.Component });
40985
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FeedsComponent, isStandalone: false, selector: "app-feed", inputs: { debug: "debug", mobile: "mobile", showHeader: "showHeader", showReadMoreButton: "showReadMoreButton", headerColor: "headerColor", cardColor: "cardColor", shape: "shape", class: "class", itemId: "itemId", filterItem: "filterItem", feeds: "feeds", urls: "urls", maxAgeInMonths: "maxAgeInMonths", maxContentLength: "maxContentLength" }, outputs: { editItem: "editItem", deleteItem: "deleteItem" }, host: { properties: { "class": "this.hostClass" } }, providers: [RxState], viewQueries: [{ propertyName: "modal", first: true, predicate: ["modal"], descendants: true }, { propertyName: "feedContent", first: true, predicate: ["feedContent"], descendants: true }, { propertyName: "feedItems", first: true, predicate: ["feedItems"], descendants: true }], ngImport: i0, template: "<!-- debug -->\n@if (debug) {\n <app-debug [title]=\"'Feed'\">\n <p>\n hasFeeds?: {{ hasFeeds$ | async }}\n <br />\n urls: {{ urls$ | async | json }}\n <br />\n shape: {{ shape }}\n </p>\n </app-debug>\n}\n\n@let feeds = feeds$ | async;\n@let userId = userId$ | async;\n\n@if (feeds | isNotEmptyArray) {\n <!-- top header -->\n @if (showHeader && (feeds | arrayFirst); as feed) {\n <ion-item lines=\"none\" [color]=\"headerColor\" class=\"feed-header shape-{{ shape }}\">\n <ion-icon slot=\"start\" name=\"megaphone\"></ion-icon>\n <ion-label>\n <b>{{ feed.title || ('SOCIAL.FEED.NEWS' | translate) }}</b>\n </ion-label>\n <ion-button slot=\"end\" fill=\"clear\" (click)=\"openFeedHome()\" shape=\"\">\n <ion-text>{{ 'SOCIAL.FEED.SHOW_ALL_FEED' | translate }}</ion-text>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n <!-- scrollable (without visible scrollbar), with a progressive fade at the bottom, when more items are below -->\n <div\n #feedContent\n class=\"feed-content shape-{{ shape }} ion-no-padding\"\n [class.has-header]=\"showHeader\"\n (scroll)=\"updateOverflow()\"\n >\n <div #feedItems class=\"feed-items\">\n <!-- visible items, of all feeds (first/last are computed on the visible items, e.g. when filtered by itemId) -->\n @let entries = feeds | map: getVisibleEntries : filterItem;\n @for (entry of entries; track entry.feed.feed_url + '#' + entry.item.id; let first = $first; let last = $last) {\n @let feed = entry.feed;\n @let item = entry.item;\n <ion-card\n [class.first]=\"first\"\n [class.last]=\"last\"\n [color]=\"cardColor !== 'light-transparent' ? cardColor : undefined\"\n class=\"feed-item-card\"\n >\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n <!-- Authors -->\n @for (author of item.authors || feed.authors; track author) {\n @if (author.name || author.avatar) {\n <ion-chip (click)=\"openUrl(author.url)\" tappable>\n @if (author.avatar) {\n <ion-avatar>\n <ion-img [src]=\"author.avatar\" [alt]=\"author.name\"></ion-img>\n </ion-avatar>\n }\n @if (author.name) {\n <ion-label class=\"author\">{{ author.name }}</ion-label>\n }\n </ion-chip>\n }\n }\n <ion-note class=\"ion-float-end\">\n <small>{{ item.date_published | dateFromNow }}</small>\n </ion-note>\n </ion-card-subtitle>\n\n <!-- title -->\n <ion-card-title (click)=\"openFeedItem(item, feed)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <ion-text class=\"tags\">\n @for (tag of tags; track tag; let last = $last) {\n <a (click)=\"openTag(feed, tag)\" tappable>\n <ion-text>#{{ tag }}</ion-text>\n </a>\n @if (!last) {\n &nbsp;\n }\n }\n </ion-text>\n }\n </ion-card-header>\n\n <!-- Feed content -->\n <ion-card-content>\n <ion-text [feed]=\"item.url || feed.feed_url\">\n @if (item.content_html) {\n <p [innerHTML]=\"item.content_html\"></p>\n } @else if (item.content_text) {\n <p>\n <markdown [data]=\"item.content_text\" emoji></markdown>\n </p>\n }\n </ion-text>\n </ion-card-content>\n\n <!-- Attachments -->\n @if (item.attachments | isNotEmptyArray) {\n <div class=\"feed-attachments\" [class.mobile]=\"mobile\">\n @for (attachment of item.attachments; track attachment.url) {\n @let filename = attachment | map: getAttachmentFilename;\n @let canPreview = attachment | map: canPreviewAttachment;\n @let action =\n (canPreview ? 'SOCIAL.FEED.ATTACHMENTS.BTN_PREVIEW' : 'SOCIAL.FEED.ATTACHMENTS.BTN_DOWNLOAD')\n | translate;\n <ion-chip\n class=\"feed-attachment\"\n [class.has-download]=\"canPreview\"\n [outline]=\"true\"\n [title]=\"\n filename +\n (attachment.size_in_bytes ? ' (' + (attachment.size_in_bytes | fileSize) + ')' : '') +\n ' - ' +\n action\n \"\n (click)=\"openAttachment($event, attachment)\"\n tappable\n >\n <mat-icon class=\"attachment-icon\">{{ attachment | map: getAttachmentIcon }}</mat-icon>\n <ion-label class=\"attachment-name\">{{ filename }}</ion-label>\n @if (canPreview) {\n <!-- download part (separated, visible on hover): preview is the default action -->\n <span\n class=\"attachment-download\"\n role=\"button\"\n [title]=\"'SOCIAL.FEED.ATTACHMENTS.BTN_DOWNLOAD' | translate\"\n (click)=\"downloadAttachment($event, attachment)\"\n >\n <mat-icon>download</mat-icon>\n </span>\n }\n </ion-chip>\n }\n </div>\n }\n\n @let editable = canEditItem(item, userId, feed);\n @if (editable || showReadMoreButton) {\n @if (editable) {\n <!-- Delete button (visible hover)-->\n <button\n mat-icon-button\n (click)=\"onDeleteItem($event, item)\"\n class=\"visible-hover ion-float-start\"\n [title]=\"!showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n <!-- Edit button (visible hover) -->\n <ion-button (click)=\"onEditItem($event, item)\" class=\"visible-hover ion-float-start\" fill=\"clear\">\n <mat-icon slot=\"start\">edit</mat-icon>\n <!-- <ion-icon name=\"pencil\" slot=\"start\"></ion-icon>-->\n <ion-text>{{ 'COMMON.BTN_EDIT' | translate }}</ion-text>\n </ion-button>\n }\n @if (showReadMoreButton) {\n <ion-button (click)=\"openFeedItem(item, feed)\" class=\"ion-float-end\" fill=\"clear\">\n <ion-label>{{ (item.truncated ? 'SOCIAL.FEED.READ_MORE' : 'COMMON.BTN_SHOW') | translate }}</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n }\n }\n </ion-card>\n }\n </div>\n </div>\n}\n\n<!-- Details modal -->\n<ion-modal #modal [showBackdrop]=\"false\" class=\"stack-modal\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ 'SOCIAL.FEED.NEWS' | translate }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"modal.dismiss()\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <div class=\"ion-padding\">\n <app-feed\n [urls]=\"urls\"\n [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [maxContentLength]=\"-1\"\n [maxAgeInMonths]=\"-1\"\n [itemId]=\"modalItemId\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [":host{display:flex;flex-direction:column;min-height:0;height:calc(100% - 10px);max-height:fit-content;overflow:hidden;--feed-header-height: 48px;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 3px 1px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f;-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-top:0;margin-bottom:10px;--feed-border-radius: 4px;border-radius:var(--feed-border-radius)}:host.shape-round{--feed-border-radius: 12px}:host{--ion-card-background: rgba(var(--ion-background-color-rgb), .6)}ion-button{text-transform:unset;--color: rgba(var(--ion-color-contrast-rgb), .7)}ion-item.feed-header{flex:none;height:var(--feed-header-height);margin:0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border-radius:var(--feed-border-radius) var(--feed-border-radius) 0 0}ion-item.feed-header ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed-content{flex:1 1 auto;min-height:0;overflow-y:auto;--margin-bottom: 8px;--feed-fade-height: 64px;scrollbar-width:none}.feed-content::-webkit-scrollbar{display:none}.feed-content ion-card.feed-item-card{--top-radius: 4px;--bottom-radius: 4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 var(--margin-bottom) 0;border-radius:var(--top-radius) var(--top-radius) var(--bottom-radius) var(--bottom-radius);--ion-card-color-contrast-rgb: var(--ion-color-contrast-rgb, var(--ion-color-dark-rgb, 0, 0, 0));--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card.first{--top-radius: 0}.feed-content ion-card.feed-item-card.last{--margin-bottom: 0;--bottom-radius: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-title{--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-avatar{--color: rgba(var(--ion-card-color-contrast-rgb), .6);border:1px solid var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-label{--color: rgba(var(--ion-card-color-contrast-rgb), .8);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-note{--color: rgba(var(--ion-card-color-contrast-rgb), .6);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header .tags{display:inline}.feed-content ion-card.feed-item-card ion-card-header .tags a{color:rgba(var(--ion-card-color-contrast-rgb),.6)!important}.feed-content ion-card.feed-item-card ion-card-content ion-text ::ng-deep img{max-width:100%;height:auto!important}.feed-content ion-card.feed-item-card .visible-hover{opacity:0;transition:opacity .2s ease-in-out}.feed-content ion-card.feed-item-card:hover .visible-hover{opacity:1}.feed-attachments{display:flex;flex-wrap:wrap;gap:4px;padding:0 16px 8px}.feed-attachments .feed-attachment{--feed-attachment-height: 22px;--feed-attachment-icon-size: 14px;--feed-attachment-padding: 6px;max-width:100%;height:var(--feed-attachment-height);min-height:var(--feed-attachment-height);margin:0;padding:0 var(--feed-attachment-padding);font-size:12px;border-width:1px;overflow:hidden;color:var(--ion-text-color, #000);border-color:rgba(var(--ion-text-color-rgb, 0, 0, 0),.4)}.feed-attachments .feed-attachment mat-icon{flex-shrink:0;width:var(--feed-attachment-icon-size);height:var(--feed-attachment-icon-size);font-size:var(--feed-attachment-icon-size);line-height:var(--feed-attachment-icon-size)}.feed-attachments .feed-attachment .attachment-icon{margin-inline-end:4px}.feed-attachments .feed-attachment .attachment-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.feed-attachments .feed-attachment .attachment-download{display:flex;align-items:center;align-self:stretch;flex-shrink:0;overflow:hidden;max-width:0;margin-inline-start:0;padding:0;border-inline-start:0 solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.3);opacity:0;transition:max-width .15s ease-in-out,opacity .15s ease-in-out,background-color .15s ease-in-out}.feed-attachments .feed-attachment.has-download:hover{padding-inline-end:0}.feed-attachments .feed-attachment:hover .attachment-download{max-width:calc(var(--feed-attachment-icon-size) + 2 * var(--feed-attachment-padding) + 1px);margin-inline-start:var(--feed-attachment-padding);padding:0 var(--feed-attachment-padding);border-inline-start-width:1px;opacity:1}.feed-attachments .feed-attachment .attachment-download:hover{color:var(--ion-color-primary);background-color:rgba(var(--ion-color-primary-rgb, 0, 0, 0),.12)}.feed-attachments.mobile .feed-attachment{--feed-attachment-height: 32px;--feed-attachment-icon-size: 18px;--feed-attachment-padding: 10px;font-size:14px}.feed-attachments.mobile .feed-attachment.has-download{padding-inline-end:0}.feed-attachments.mobile .feed-attachment .attachment-download{max-width:calc(var(--feed-attachment-icon-size) + 2 * var(--feed-attachment-padding) + 1px);margin-inline-start:var(--feed-attachment-padding);padding:0 var(--feed-attachment-padding);border-inline-start-width:1px;opacity:1}\n"], dependencies: [{ kind: "component", type: i2$2.IonAvatar, selector: "ion-avatar" }, { kind: "component", type: i2$2.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: i2$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$2.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$2.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonChip, selector: "ion-chip", inputs: ["color", "disabled", "mode", "outline"] }, { kind: "component", type: i2$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonModal, selector: "ion-modal" }, { kind: "component", type: i2$2.IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i4$3.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i6.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }, { kind: "directive", type: MarkdownDirective, selector: "markdown,[markdown]" }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "urls", "maxAgeInMonths", "maxContentLength"], outputs: ["editItem", "deleteItem"] }, { kind: "directive", type: FeedDirective, selector: "feed,[feed]", inputs: ["feed"] }, { kind: "pipe", type: i2$3.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$3.JsonPipe, name: "json" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: FileSizePipe, name: "fileSize" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: ArrayFirstPipe, name: "arrayFirst" }, { kind: "pipe", type: MapPipe, name: "map" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
41061
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: FeedsComponent, isStandalone: false, selector: "app-feed", inputs: { debug: "debug", mobile: "mobile", showHeader: "showHeader", showReadMoreButton: "showReadMoreButton", headerColor: "headerColor", cardColor: "cardColor", shape: "shape", class: "class", itemId: "itemId", filterItem: "filterItem", feeds: "feeds", urls: "urls", maxAgeInMonths: "maxAgeInMonths", maxContentLength: "maxContentLength" }, outputs: { editItem: "editItem", deleteItem: "deleteItem" }, host: { properties: { "class": "this.hostClass" } }, providers: [RxState], viewQueries: [{ propertyName: "modal", first: true, predicate: ["modal"], descendants: true }, { propertyName: "feedContent", first: true, predicate: ["feedContent"], descendants: true }, { propertyName: "feedItems", first: true, predicate: ["feedItems"], descendants: true }], ngImport: i0, template: "<!-- debug -->\n@if (debug) {\n <app-debug [title]=\"'Feed'\">\n <p>\n hasFeeds?: {{ hasFeeds$ | async }}\n <br />\n urls: {{ urls$ | async | json }}\n <br />\n shape: {{ shape }}\n </p>\n </app-debug>\n}\n\n@let feeds = feeds$ | async;\n@let userId = userId$ | async;\n\n@if (feeds | isNotEmptyArray) {\n <!-- top header -->\n @if (showHeader && (feeds | arrayFirst); as feed) {\n <ion-item lines=\"none\" [color]=\"headerColor\" class=\"feed-header shape-{{ shape }}\">\n <ion-icon slot=\"start\" name=\"megaphone\"></ion-icon>\n <ion-label>\n <b>{{ feed.title || ('SOCIAL.FEED.NEWS' | translate) }}</b>\n </ion-label>\n <ion-button slot=\"end\" fill=\"clear\" (click)=\"openFeedHome()\" shape=\"\">\n <ion-text>{{ 'SOCIAL.FEED.SHOW_ALL_FEED' | translate }}</ion-text>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n <!-- scrollable (without visible scrollbar), with a progressive fade at the bottom, when more items are below -->\n <div\n #feedContent\n class=\"feed-content shape-{{ shape }} ion-no-padding\"\n [class.has-header]=\"showHeader\"\n (scroll)=\"updateOverflow()\"\n >\n <div #feedItems class=\"feed-items\">\n <!-- visible items, of all feeds (first/last are computed on the visible items, e.g. when filtered by itemId) -->\n @let entries = feeds | map: getVisibleEntries : filterItem;\n @for (entry of entries; track entry.feed.feed_url + '#' + entry.item.id; let first = $first; let last = $last) {\n @let feed = entry.feed;\n @let item = entry.item;\n <ion-card\n [class.first]=\"first\"\n [class.last]=\"last\"\n [color]=\"cardColor !== 'light-transparent' ? cardColor : undefined\"\n class=\"feed-item-card\"\n >\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n <!-- Authors -->\n @for (author of item.authors || feed.authors; track author) {\n @if (author.name || author.avatar) {\n <ion-chip (click)=\"openUrl(author.url)\" tappable>\n @if (author.avatar) {\n <ion-avatar>\n <ion-img [src]=\"author.avatar\" [alt]=\"author.name\"></ion-img>\n </ion-avatar>\n }\n @if (author.name) {\n <ion-label class=\"author\">{{ author.name }}</ion-label>\n }\n </ion-chip>\n }\n }\n <ion-note class=\"ion-float-end\">\n <small>{{ item.date_published | dateFromNow }}</small>\n </ion-note>\n </ion-card-subtitle>\n\n <!-- title -->\n <ion-card-title (click)=\"openFeedItem(item, feed)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <ion-text class=\"tags\">\n @for (tag of tags; track tag; let last = $last) {\n <a (click)=\"openTag(feed, tag)\" tappable>\n <ion-text>#{{ tag }}</ion-text>\n </a>\n @if (!last) {\n &nbsp;\n }\n }\n </ion-text>\n }\n </ion-card-header>\n\n <!-- Feed content -->\n <ion-card-content>\n <ion-text [feed]=\"item.url || feed.feed_url\">\n @if (item.content_html) {\n <p [innerHTML]=\"item.content_html\"></p>\n } @else if (item.content_text) {\n <p>\n <markdown [data]=\"item.content_text\" emoji></markdown>\n </p>\n }\n </ion-text>\n </ion-card-content>\n\n <!-- Attachments -->\n @if (item.attachments | isNotEmptyArray) {\n <div class=\"feed-attachments\">\n @for (attachment of item.attachments; track attachment.url) {\n <app-file-attachment class=\"feed-attachment\" [attachment]=\"attachment | map: toFileAttachment\" [mobile]=\"mobile\"></app-file-attachment>\n }\n </div>\n }\n\n @let editable = canEditItem(item, userId, feed);\n @if (editable || showReadMoreButton) {\n @if (editable) {\n <!-- Delete button (visible hover)-->\n <button\n mat-icon-button\n (click)=\"onDeleteItem($event, item)\"\n class=\"visible-hover ion-float-start\"\n [title]=\"!showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n <!-- Edit button (visible hover) -->\n <ion-button (click)=\"onEditItem($event, item)\" class=\"visible-hover ion-float-start\" fill=\"clear\">\n <mat-icon slot=\"start\">edit</mat-icon>\n <!-- <ion-icon name=\"pencil\" slot=\"start\"></ion-icon>-->\n <ion-text>{{ 'COMMON.BTN_EDIT' | translate }}</ion-text>\n </ion-button>\n }\n @if (showReadMoreButton) {\n <ion-button (click)=\"openFeedItem(item, feed)\" class=\"ion-float-end\" fill=\"clear\">\n <ion-label>{{ (item.truncated ? 'SOCIAL.FEED.READ_MORE' : 'COMMON.BTN_SHOW') | translate }}</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n }\n }\n </ion-card>\n }\n </div>\n </div>\n}\n\n<!-- Details modal -->\n<ion-modal #modal [showBackdrop]=\"false\" class=\"stack-modal\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ 'SOCIAL.FEED.NEWS' | translate }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"modal.dismiss()\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <div class=\"ion-padding\">\n <app-feed\n [urls]=\"urls\"\n [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [maxContentLength]=\"-1\"\n [maxAgeInMonths]=\"-1\"\n [itemId]=\"modalItemId\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [":host{display:flex;flex-direction:column;min-height:0;height:calc(100% - 10px);max-height:fit-content;overflow:hidden;--feed-header-height: 48px;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 3px 1px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f;-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-top:0;margin-bottom:10px;--feed-border-radius: 4px;border-radius:var(--feed-border-radius)}:host.shape-round{--feed-border-radius: 12px}:host{--ion-card-background: rgba(var(--ion-background-color-rgb), .6)}ion-button{text-transform:unset;--color: rgba(var(--ion-color-contrast-rgb), .7)}ion-item.feed-header{flex:none;height:var(--feed-header-height);margin:0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border-radius:var(--feed-border-radius) var(--feed-border-radius) 0 0}ion-item.feed-header ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed-content{flex:1 1 auto;min-height:0;overflow-y:auto;--margin-bottom: 8px;--feed-fade-height: 64px;scrollbar-width:none}.feed-content::-webkit-scrollbar{display:none}.feed-content ion-card.feed-item-card{--top-radius: 4px;--bottom-radius: 4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 var(--margin-bottom) 0;border-radius:var(--top-radius) var(--top-radius) var(--bottom-radius) var(--bottom-radius);--ion-card-color-contrast-rgb: var(--ion-color-contrast-rgb, var(--ion-color-dark-rgb, 0, 0, 0));--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card.first{--top-radius: 0}.feed-content ion-card.feed-item-card.last{--margin-bottom: 0;--bottom-radius: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-title{--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-avatar{--color: rgba(var(--ion-card-color-contrast-rgb), .6);border:1px solid var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-label{--color: rgba(var(--ion-card-color-contrast-rgb), .8);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-note{--color: rgba(var(--ion-card-color-contrast-rgb), .6);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header .tags{display:inline}.feed-content ion-card.feed-item-card ion-card-header .tags a{color:rgba(var(--ion-card-color-contrast-rgb),.6)!important}.feed-content ion-card.feed-item-card ion-card-content ion-text ::ng-deep img{max-width:100%;height:auto!important}.feed-content ion-card.feed-item-card .visible-hover{opacity:0;transition:opacity .2s ease-in-out}.feed-content ion-card.feed-item-card:hover .visible-hover{opacity:1}.feed-attachments{display:flex;flex-wrap:wrap;gap:4px;padding:0 16px 8px}\n"], dependencies: [{ kind: "component", type: i2$2.IonAvatar, selector: "ion-avatar" }, { kind: "component", type: i2$2.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: i2$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$2.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$2.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonChip, selector: "ion-chip", inputs: ["color", "disabled", "mode", "outline"] }, { kind: "component", type: i2$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonModal, selector: "ion-modal" }, { kind: "component", type: i2$2.IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i4$3.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: i6.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }, { kind: "directive", type: MarkdownDirective, selector: "markdown,[markdown]" }, { kind: "component", type: FileAttachmentComponent, selector: "app-file-attachment", inputs: ["attachment", "mobile"] }, { kind: "component", type: FeedsComponent, selector: "app-feed", inputs: ["debug", "mobile", "showHeader", "showReadMoreButton", "headerColor", "cardColor", "shape", "class", "itemId", "filterItem", "feeds", "urls", "maxAgeInMonths", "maxContentLength"], outputs: ["editItem", "deleteItem"] }, { kind: "directive", type: FeedDirective, selector: "feed,[feed]", inputs: ["feed"] }, { kind: "pipe", type: i2$3.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$3.JsonPipe, name: "json" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: ArrayFirstPipe, name: "arrayFirst" }, { kind: "pipe", type: MapPipe, name: "map" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
40986
41062
  }
40987
41063
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FeedsComponent, decorators: [{
40988
41064
  type: Component,
40989
- args: [{ selector: 'app-feed', providers: [RxState], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- debug -->\n@if (debug) {\n <app-debug [title]=\"'Feed'\">\n <p>\n hasFeeds?: {{ hasFeeds$ | async }}\n <br />\n urls: {{ urls$ | async | json }}\n <br />\n shape: {{ shape }}\n </p>\n </app-debug>\n}\n\n@let feeds = feeds$ | async;\n@let userId = userId$ | async;\n\n@if (feeds | isNotEmptyArray) {\n <!-- top header -->\n @if (showHeader && (feeds | arrayFirst); as feed) {\n <ion-item lines=\"none\" [color]=\"headerColor\" class=\"feed-header shape-{{ shape }}\">\n <ion-icon slot=\"start\" name=\"megaphone\"></ion-icon>\n <ion-label>\n <b>{{ feed.title || ('SOCIAL.FEED.NEWS' | translate) }}</b>\n </ion-label>\n <ion-button slot=\"end\" fill=\"clear\" (click)=\"openFeedHome()\" shape=\"\">\n <ion-text>{{ 'SOCIAL.FEED.SHOW_ALL_FEED' | translate }}</ion-text>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n <!-- scrollable (without visible scrollbar), with a progressive fade at the bottom, when more items are below -->\n <div\n #feedContent\n class=\"feed-content shape-{{ shape }} ion-no-padding\"\n [class.has-header]=\"showHeader\"\n (scroll)=\"updateOverflow()\"\n >\n <div #feedItems class=\"feed-items\">\n <!-- visible items, of all feeds (first/last are computed on the visible items, e.g. when filtered by itemId) -->\n @let entries = feeds | map: getVisibleEntries : filterItem;\n @for (entry of entries; track entry.feed.feed_url + '#' + entry.item.id; let first = $first; let last = $last) {\n @let feed = entry.feed;\n @let item = entry.item;\n <ion-card\n [class.first]=\"first\"\n [class.last]=\"last\"\n [color]=\"cardColor !== 'light-transparent' ? cardColor : undefined\"\n class=\"feed-item-card\"\n >\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n <!-- Authors -->\n @for (author of item.authors || feed.authors; track author) {\n @if (author.name || author.avatar) {\n <ion-chip (click)=\"openUrl(author.url)\" tappable>\n @if (author.avatar) {\n <ion-avatar>\n <ion-img [src]=\"author.avatar\" [alt]=\"author.name\"></ion-img>\n </ion-avatar>\n }\n @if (author.name) {\n <ion-label class=\"author\">{{ author.name }}</ion-label>\n }\n </ion-chip>\n }\n }\n <ion-note class=\"ion-float-end\">\n <small>{{ item.date_published | dateFromNow }}</small>\n </ion-note>\n </ion-card-subtitle>\n\n <!-- title -->\n <ion-card-title (click)=\"openFeedItem(item, feed)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <ion-text class=\"tags\">\n @for (tag of tags; track tag; let last = $last) {\n <a (click)=\"openTag(feed, tag)\" tappable>\n <ion-text>#{{ tag }}</ion-text>\n </a>\n @if (!last) {\n &nbsp;\n }\n }\n </ion-text>\n }\n </ion-card-header>\n\n <!-- Feed content -->\n <ion-card-content>\n <ion-text [feed]=\"item.url || feed.feed_url\">\n @if (item.content_html) {\n <p [innerHTML]=\"item.content_html\"></p>\n } @else if (item.content_text) {\n <p>\n <markdown [data]=\"item.content_text\" emoji></markdown>\n </p>\n }\n </ion-text>\n </ion-card-content>\n\n <!-- Attachments -->\n @if (item.attachments | isNotEmptyArray) {\n <div class=\"feed-attachments\" [class.mobile]=\"mobile\">\n @for (attachment of item.attachments; track attachment.url) {\n @let filename = attachment | map: getAttachmentFilename;\n @let canPreview = attachment | map: canPreviewAttachment;\n @let action =\n (canPreview ? 'SOCIAL.FEED.ATTACHMENTS.BTN_PREVIEW' : 'SOCIAL.FEED.ATTACHMENTS.BTN_DOWNLOAD')\n | translate;\n <ion-chip\n class=\"feed-attachment\"\n [class.has-download]=\"canPreview\"\n [outline]=\"true\"\n [title]=\"\n filename +\n (attachment.size_in_bytes ? ' (' + (attachment.size_in_bytes | fileSize) + ')' : '') +\n ' - ' +\n action\n \"\n (click)=\"openAttachment($event, attachment)\"\n tappable\n >\n <mat-icon class=\"attachment-icon\">{{ attachment | map: getAttachmentIcon }}</mat-icon>\n <ion-label class=\"attachment-name\">{{ filename }}</ion-label>\n @if (canPreview) {\n <!-- download part (separated, visible on hover): preview is the default action -->\n <span\n class=\"attachment-download\"\n role=\"button\"\n [title]=\"'SOCIAL.FEED.ATTACHMENTS.BTN_DOWNLOAD' | translate\"\n (click)=\"downloadAttachment($event, attachment)\"\n >\n <mat-icon>download</mat-icon>\n </span>\n }\n </ion-chip>\n }\n </div>\n }\n\n @let editable = canEditItem(item, userId, feed);\n @if (editable || showReadMoreButton) {\n @if (editable) {\n <!-- Delete button (visible hover)-->\n <button\n mat-icon-button\n (click)=\"onDeleteItem($event, item)\"\n class=\"visible-hover ion-float-start\"\n [title]=\"!showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n <!-- Edit button (visible hover) -->\n <ion-button (click)=\"onEditItem($event, item)\" class=\"visible-hover ion-float-start\" fill=\"clear\">\n <mat-icon slot=\"start\">edit</mat-icon>\n <!-- <ion-icon name=\"pencil\" slot=\"start\"></ion-icon>-->\n <ion-text>{{ 'COMMON.BTN_EDIT' | translate }}</ion-text>\n </ion-button>\n }\n @if (showReadMoreButton) {\n <ion-button (click)=\"openFeedItem(item, feed)\" class=\"ion-float-end\" fill=\"clear\">\n <ion-label>{{ (item.truncated ? 'SOCIAL.FEED.READ_MORE' : 'COMMON.BTN_SHOW') | translate }}</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n }\n }\n </ion-card>\n }\n </div>\n </div>\n}\n\n<!-- Details modal -->\n<ion-modal #modal [showBackdrop]=\"false\" class=\"stack-modal\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ 'SOCIAL.FEED.NEWS' | translate }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"modal.dismiss()\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <div class=\"ion-padding\">\n <app-feed\n [urls]=\"urls\"\n [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [maxContentLength]=\"-1\"\n [maxAgeInMonths]=\"-1\"\n [itemId]=\"modalItemId\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [":host{display:flex;flex-direction:column;min-height:0;height:calc(100% - 10px);max-height:fit-content;overflow:hidden;--feed-header-height: 48px;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 3px 1px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f;-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-top:0;margin-bottom:10px;--feed-border-radius: 4px;border-radius:var(--feed-border-radius)}:host.shape-round{--feed-border-radius: 12px}:host{--ion-card-background: rgba(var(--ion-background-color-rgb), .6)}ion-button{text-transform:unset;--color: rgba(var(--ion-color-contrast-rgb), .7)}ion-item.feed-header{flex:none;height:var(--feed-header-height);margin:0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border-radius:var(--feed-border-radius) var(--feed-border-radius) 0 0}ion-item.feed-header ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed-content{flex:1 1 auto;min-height:0;overflow-y:auto;--margin-bottom: 8px;--feed-fade-height: 64px;scrollbar-width:none}.feed-content::-webkit-scrollbar{display:none}.feed-content ion-card.feed-item-card{--top-radius: 4px;--bottom-radius: 4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 var(--margin-bottom) 0;border-radius:var(--top-radius) var(--top-radius) var(--bottom-radius) var(--bottom-radius);--ion-card-color-contrast-rgb: var(--ion-color-contrast-rgb, var(--ion-color-dark-rgb, 0, 0, 0));--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card.first{--top-radius: 0}.feed-content ion-card.feed-item-card.last{--margin-bottom: 0;--bottom-radius: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-title{--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-avatar{--color: rgba(var(--ion-card-color-contrast-rgb), .6);border:1px solid var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-label{--color: rgba(var(--ion-card-color-contrast-rgb), .8);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-note{--color: rgba(var(--ion-card-color-contrast-rgb), .6);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header .tags{display:inline}.feed-content ion-card.feed-item-card ion-card-header .tags a{color:rgba(var(--ion-card-color-contrast-rgb),.6)!important}.feed-content ion-card.feed-item-card ion-card-content ion-text ::ng-deep img{max-width:100%;height:auto!important}.feed-content ion-card.feed-item-card .visible-hover{opacity:0;transition:opacity .2s ease-in-out}.feed-content ion-card.feed-item-card:hover .visible-hover{opacity:1}.feed-attachments{display:flex;flex-wrap:wrap;gap:4px;padding:0 16px 8px}.feed-attachments .feed-attachment{--feed-attachment-height: 22px;--feed-attachment-icon-size: 14px;--feed-attachment-padding: 6px;max-width:100%;height:var(--feed-attachment-height);min-height:var(--feed-attachment-height);margin:0;padding:0 var(--feed-attachment-padding);font-size:12px;border-width:1px;overflow:hidden;color:var(--ion-text-color, #000);border-color:rgba(var(--ion-text-color-rgb, 0, 0, 0),.4)}.feed-attachments .feed-attachment mat-icon{flex-shrink:0;width:var(--feed-attachment-icon-size);height:var(--feed-attachment-icon-size);font-size:var(--feed-attachment-icon-size);line-height:var(--feed-attachment-icon-size)}.feed-attachments .feed-attachment .attachment-icon{margin-inline-end:4px}.feed-attachments .feed-attachment .attachment-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.feed-attachments .feed-attachment .attachment-download{display:flex;align-items:center;align-self:stretch;flex-shrink:0;overflow:hidden;max-width:0;margin-inline-start:0;padding:0;border-inline-start:0 solid rgba(var(--ion-text-color-rgb, 0, 0, 0),.3);opacity:0;transition:max-width .15s ease-in-out,opacity .15s ease-in-out,background-color .15s ease-in-out}.feed-attachments .feed-attachment.has-download:hover{padding-inline-end:0}.feed-attachments .feed-attachment:hover .attachment-download{max-width:calc(var(--feed-attachment-icon-size) + 2 * var(--feed-attachment-padding) + 1px);margin-inline-start:var(--feed-attachment-padding);padding:0 var(--feed-attachment-padding);border-inline-start-width:1px;opacity:1}.feed-attachments .feed-attachment .attachment-download:hover{color:var(--ion-color-primary);background-color:rgba(var(--ion-color-primary-rgb, 0, 0, 0),.12)}.feed-attachments.mobile .feed-attachment{--feed-attachment-height: 32px;--feed-attachment-icon-size: 18px;--feed-attachment-padding: 10px;font-size:14px}.feed-attachments.mobile .feed-attachment.has-download{padding-inline-end:0}.feed-attachments.mobile .feed-attachment .attachment-download{max-width:calc(var(--feed-attachment-icon-size) + 2 * var(--feed-attachment-padding) + 1px);margin-inline-start:var(--feed-attachment-padding);padding:0 var(--feed-attachment-padding);border-inline-start-width:1px;opacity:1}\n"] }]
41065
+ args: [{ selector: 'app-feed', providers: [RxState], changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- debug -->\n@if (debug) {\n <app-debug [title]=\"'Feed'\">\n <p>\n hasFeeds?: {{ hasFeeds$ | async }}\n <br />\n urls: {{ urls$ | async | json }}\n <br />\n shape: {{ shape }}\n </p>\n </app-debug>\n}\n\n@let feeds = feeds$ | async;\n@let userId = userId$ | async;\n\n@if (feeds | isNotEmptyArray) {\n <!-- top header -->\n @if (showHeader && (feeds | arrayFirst); as feed) {\n <ion-item lines=\"none\" [color]=\"headerColor\" class=\"feed-header shape-{{ shape }}\">\n <ion-icon slot=\"start\" name=\"megaphone\"></ion-icon>\n <ion-label>\n <b>{{ feed.title || ('SOCIAL.FEED.NEWS' | translate) }}</b>\n </ion-label>\n <ion-button slot=\"end\" fill=\"clear\" (click)=\"openFeedHome()\" shape=\"\">\n <ion-text>{{ 'SOCIAL.FEED.SHOW_ALL_FEED' | translate }}</ion-text>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n <!-- scrollable (without visible scrollbar), with a progressive fade at the bottom, when more items are below -->\n <div\n #feedContent\n class=\"feed-content shape-{{ shape }} ion-no-padding\"\n [class.has-header]=\"showHeader\"\n (scroll)=\"updateOverflow()\"\n >\n <div #feedItems class=\"feed-items\">\n <!-- visible items, of all feeds (first/last are computed on the visible items, e.g. when filtered by itemId) -->\n @let entries = feeds | map: getVisibleEntries : filterItem;\n @for (entry of entries; track entry.feed.feed_url + '#' + entry.item.id; let first = $first; let last = $last) {\n @let feed = entry.feed;\n @let item = entry.item;\n <ion-card\n [class.first]=\"first\"\n [class.last]=\"last\"\n [color]=\"cardColor !== 'light-transparent' ? cardColor : undefined\"\n class=\"feed-item-card\"\n >\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n <!-- Authors -->\n @for (author of item.authors || feed.authors; track author) {\n @if (author.name || author.avatar) {\n <ion-chip (click)=\"openUrl(author.url)\" tappable>\n @if (author.avatar) {\n <ion-avatar>\n <ion-img [src]=\"author.avatar\" [alt]=\"author.name\"></ion-img>\n </ion-avatar>\n }\n @if (author.name) {\n <ion-label class=\"author\">{{ author.name }}</ion-label>\n }\n </ion-chip>\n }\n }\n <ion-note class=\"ion-float-end\">\n <small>{{ item.date_published | dateFromNow }}</small>\n </ion-note>\n </ion-card-subtitle>\n\n <!-- title -->\n <ion-card-title (click)=\"openFeedItem(item, feed)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <ion-text class=\"tags\">\n @for (tag of tags; track tag; let last = $last) {\n <a (click)=\"openTag(feed, tag)\" tappable>\n <ion-text>#{{ tag }}</ion-text>\n </a>\n @if (!last) {\n &nbsp;\n }\n }\n </ion-text>\n }\n </ion-card-header>\n\n <!-- Feed content -->\n <ion-card-content>\n <ion-text [feed]=\"item.url || feed.feed_url\">\n @if (item.content_html) {\n <p [innerHTML]=\"item.content_html\"></p>\n } @else if (item.content_text) {\n <p>\n <markdown [data]=\"item.content_text\" emoji></markdown>\n </p>\n }\n </ion-text>\n </ion-card-content>\n\n <!-- Attachments -->\n @if (item.attachments | isNotEmptyArray) {\n <div class=\"feed-attachments\">\n @for (attachment of item.attachments; track attachment.url) {\n <app-file-attachment class=\"feed-attachment\" [attachment]=\"attachment | map: toFileAttachment\" [mobile]=\"mobile\"></app-file-attachment>\n }\n </div>\n }\n\n @let editable = canEditItem(item, userId, feed);\n @if (editable || showReadMoreButton) {\n @if (editable) {\n <!-- Delete button (visible hover)-->\n <button\n mat-icon-button\n (click)=\"onDeleteItem($event, item)\"\n class=\"visible-hover ion-float-start\"\n [title]=\"!showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n [matTooltip]=\"showTooltip ? ('COMMON.BTN_DELETE' | translate) : ''\"\n >\n <mat-icon>delete</mat-icon>\n </button>\n <!-- Edit button (visible hover) -->\n <ion-button (click)=\"onEditItem($event, item)\" class=\"visible-hover ion-float-start\" fill=\"clear\">\n <mat-icon slot=\"start\">edit</mat-icon>\n <!-- <ion-icon name=\"pencil\" slot=\"start\"></ion-icon>-->\n <ion-text>{{ 'COMMON.BTN_EDIT' | translate }}</ion-text>\n </ion-button>\n }\n @if (showReadMoreButton) {\n <ion-button (click)=\"openFeedItem(item, feed)\" class=\"ion-float-end\" fill=\"clear\">\n <ion-label>{{ (item.truncated ? 'SOCIAL.FEED.READ_MORE' : 'COMMON.BTN_SHOW') | translate }}</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n }\n }\n </ion-card>\n }\n </div>\n </div>\n}\n\n<!-- Details modal -->\n<ion-modal #modal [showBackdrop]=\"false\" class=\"stack-modal\" [class.modal-large]=\"!mobile\">\n <ng-template>\n <ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-title>{{ 'SOCIAL.FEED.NEWS' | translate }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"modal.dismiss()\">\n <ion-icon slot=\"icon-only\" name=\"close\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <div class=\"ion-padding\">\n <app-feed\n [urls]=\"urls\"\n [showHeader]=\"false\"\n [showReadMoreButton]=\"false\"\n [maxContentLength]=\"-1\"\n [maxAgeInMonths]=\"-1\"\n [itemId]=\"modalItemId\"\n cardColor=\"light\"\n [debug]=\"debug\"\n ></app-feed>\n </div>\n </ion-content>\n </ng-template>\n</ion-modal>\n", styles: [":host{display:flex;flex-direction:column;min-height:0;height:calc(100% - 10px);max-height:fit-content;overflow:hidden;--feed-header-height: 48px;-webkit-box-shadow:0 3px 1px -2px rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12);box-shadow:0 3px 1px -2px #0003,0 2px 2px #00000024,0 1px 5px #0000001f;-webkit-margin-start:10px;margin-inline-start:10px;-webkit-margin-end:10px;margin-inline-end:10px;margin-top:0;margin-bottom:10px;--feed-border-radius: 4px;border-radius:var(--feed-border-radius)}:host.shape-round{--feed-border-radius: 12px}:host{--ion-card-background: rgba(var(--ion-background-color-rgb), .6)}ion-button{text-transform:unset;--color: rgba(var(--ion-color-contrast-rgb), .7)}ion-item.feed-header{flex:none;height:var(--feed-header-height);margin:0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);border-radius:var(--feed-border-radius) var(--feed-border-radius) 0 0}ion-item.feed-header ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed-content{flex:1 1 auto;min-height:0;overflow-y:auto;--margin-bottom: 8px;--feed-fade-height: 64px;scrollbar-width:none}.feed-content::-webkit-scrollbar{display:none}.feed-content ion-card.feed-item-card{--top-radius: 4px;--bottom-radius: 4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 var(--margin-bottom) 0;border-radius:var(--top-radius) var(--top-radius) var(--bottom-radius) var(--bottom-radius);--ion-card-color-contrast-rgb: var(--ion-color-contrast-rgb, var(--ion-color-dark-rgb, 0, 0, 0));--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card.first{--top-radius: 0}.feed-content ion-card.feed-item-card.last{--margin-bottom: 0;--bottom-radius: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-title{--color: rgba(var(--ion-card-color-contrast-rgb), .87)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-avatar{--color: rgba(var(--ion-card-color-contrast-rgb), .6);border:1px solid var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-chip ion-label{--color: rgba(var(--ion-card-color-contrast-rgb), .8);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header ion-card-subtitle ion-note{--color: rgba(var(--ion-card-color-contrast-rgb), .6);color:var(--color)}.feed-content ion-card.feed-item-card ion-card-header .tags{display:inline}.feed-content ion-card.feed-item-card ion-card-header .tags a{color:rgba(var(--ion-card-color-contrast-rgb),.6)!important}.feed-content ion-card.feed-item-card ion-card-content ion-text ::ng-deep img{max-width:100%;height:auto!important}.feed-content ion-card.feed-item-card .visible-hover{opacity:0;transition:opacity .2s ease-in-out}.feed-content ion-card.feed-item-card:hover .visible-hover{opacity:1}.feed-attachments{display:flex;flex-wrap:wrap;gap:4px;padding:0 16px 8px}\n"] }]
40990
41066
  }], ctorParameters: () => [{ type: LocalSettingsService }, { type: Environment, decorators: [{
40991
41067
  type: Inject,
40992
41068
  args: [ENVIRONMENT]
@@ -45061,16 +45137,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
45061
45137
 
45062
45138
  class FeedModule {
45063
45139
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FeedModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
45064
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: FeedModule, declarations: [FeedsComponent, FeedPage, FeedDirective, FeedAttachmentPreviewModal], imports: [SharedModule, SharedDebugModule, SharedMarkdownModule, MessageModule], exports: [FeedsComponent, FeedPage, FeedDirective, FeedAttachmentPreviewModal] });
45065
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FeedModule, providers: [provideChildTranslateService()], imports: [SharedModule, SharedDebugModule, SharedMarkdownModule, MessageModule] });
45140
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: FeedModule, declarations: [FeedsComponent, FeedPage, FeedDirective], imports: [SharedModule, SharedDebugModule, SharedMarkdownModule, SharedFileAttachmentModule, MessageModule], exports: [FeedsComponent, FeedPage, FeedDirective, SharedFileAttachmentModule] });
45141
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FeedModule, providers: [provideChildTranslateService()], imports: [SharedModule, SharedDebugModule, SharedMarkdownModule, SharedFileAttachmentModule, MessageModule, SharedFileAttachmentModule] });
45066
45142
  }
45067
45143
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: FeedModule, decorators: [{
45068
45144
  type: NgModule,
45069
45145
  args: [{
45070
45146
  // MessageModule: the feed service opens the message modal (to edit a feed item)
45071
- imports: [SharedModule, SharedDebugModule, SharedMarkdownModule, MessageModule],
45072
- declarations: [FeedsComponent, FeedPage, FeedDirective, FeedAttachmentPreviewModal],
45073
- exports: [FeedsComponent, FeedPage, FeedDirective, FeedAttachmentPreviewModal],
45147
+ imports: [SharedModule, SharedDebugModule, SharedMarkdownModule, SharedFileAttachmentModule, MessageModule],
45148
+ declarations: [FeedsComponent, FeedPage, FeedDirective],
45149
+ exports: [FeedsComponent, FeedPage, FeedDirective, SharedFileAttachmentModule],
45074
45150
  providers: [provideChildTranslateService()],
45075
45151
  }]
45076
45152
  }] });
@@ -58766,5 +58842,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
58766
58842
  * Generated bundle index. Do not edit.
58767
58843
  */
58768
58844
 
58769
- export { ADMIN_ICONS, APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_ACCOUNT_SERVICE, APP_ACCOUNT_SERVICE_OPTIONS, APP_CELL_SELECTION_SERVICE_CONFIG_TOKEN, APP_CELL_SELECTION_SERVICE_TOKEN, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FILE_TRANSFER_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_SERVICE, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOME_TOOLBAR_BUTTONS, APP_HOTKEYS_CONFIG, APP_INPUT_FIELD, APP_JOB_PROGRESSION_SERVICE, APP_LOCALES, APP_LOCAL_SETTINGS, APP_LOCAL_SETTINGS_OPTIONS, APP_LOCAL_STORAGE_TYPE_POLICIES, APP_LOGGING_SERVICE, APP_MENU_ITEMS, APP_MENU_OPTIONS, APP_MESSAGE_SERVICE_OPTIONS, APP_NAMED_FILTER_SERVICE, APP_PERSON_SERVICE, APP_PERSON_SERVICE_OPTIONS, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_SHOW_TOOLTIP, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TABLE_OPTIONS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, AbstractPersonService, AbstractSelectionModelPipe, AbstractTableSelectionPipe, AbstractUserEventService, Account, AccountPage, AccountService, AccountToStringPipe, AccountUtils, ActionsColumnComponent, AdminModule, AdminRoutingModule, AdminUsersModule, Alerts, AndroidOsEnvironment, AppAboutModalModule, AppAccountModule, AppAsyncTable, AppAuthForm, AppAuthModal, AppAuthModule, AppChangePasswordModule, AppChangePasswordPage, AppEditor, AppEditorOptions, AppEntityEditor, AppEntityEditorModal, AppEntityEditorModalOptions, AppEntityFormModule, AppForm, AppFormArray, AppFormButtonsBarModule, AppFormContainer, AppFormField, AppFormModule, AppFormProvider, AppFormUtils, AppGestureConfig, AppHomePageModule, AppIconComponent, AppIconModule, AppIconSelectorField, AppIconSelectorModal, AppIconSelectorModule, AppImageGalleryComponent, AppImageGallerySlideshowComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSelectUsersModal, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextFormModule, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppWebSocket, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFindPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayMapPipe, ArrayPluckPipe, ArraySlicePipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutoTooltipDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_ICONS, CORE_TESTING_PAGES, CapitalizePipe, CellIdentifierDirective, CellSelectionDirective, CellSelectionService, CellValueChangeListener, ChangeCaseToUnderscorePipe, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, DATE_ISO_PATTERNS, DATE_MATCH_REGEXP, DATE_PATTERN, DATE_UNIX_MS_TIMESTAMP, DATE_UNIX_TIMESTAMP, DEFAULT_JOIN_ARRAY_VALUES_SEPARATOR, DEFAULT_JOIN_PROPERTIES_SEPARATOR, DEFAULT_MENU_SHOW_WHEN, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLACEHOLDER_CHAR, DEFAULT_REQUIRED_COLUMNS, DateDiffDurationPipe, DateFormatPipe, DateFormatService, DateFromNowPipe, DateFromPipe, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EmptyMissingTranslationHandler, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FILE_KIND_ICONS, FeedAttachmentPreviewModal, FeedAttachmentUtils, FeedDirective, FeedModule, FeedPage, FeedService, FeedsComponent, FileDownloadService, FileResponse, FileService, FileSizePipe, FileTypeUtils, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayAtControlPipe, FormArrayAtGroupPipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetNamePipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GitlabUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, HttpUtils, IMAGE_DEFAULTS, ION_ICONS, IPosition, IconSelectorTestPage, IconSelectorTestingModule, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImageSizes, ImagesUtils, InMemoryEntitiesService, InputFieldTestPage, IsAllSelectedPipe, IsEmptySelectionPipe, IsLoginAccountPipe, IsMultipleSelectionPipe, IsNilOrBlankPipe, IsNilOrNaNPipe, IsNilPipe, IsNotAllSelectedPipe, IsNotEmptySelectionPipe, IsNotNilOrBlankPipe, IsNotNilOrNaNPipe, IsNotNilPipe, IsOnDeskPipe, IsOnFieldPipe, IsSelectedPipe, IsSingleSelectionPipe, IsValidDatePipe, JobModule, JobProgression, JobProgressionComponent, JobProgressionIcon, JobProgressionList, JobProgressionService, JobProgressionTestService, JobProgressionTestingPage, JobTestingModule, JobUtils, JsonFeedUtils, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, ListFormTestPage, ListFormTestingModule, LocalSettingsService, LogLevel, LogUtils, Logger, LoggingService, LoggingServiceModule, LongitudeFormatPipe, MASKS, MASK_RANGES, MAT_FORM_FIELD_DEFAULT_APPEARANCE, MAT_FORM_FIELD_DEFAULT_SUBSCRIPT_SIZING, MINIFY_ENTITY_FOR_LOCAL_STORAGE, MINIFY_ENTITY_FOR_POD, MOMENT_NO_TIME_PROPERTY, MapGetPipe, MapKeysPipe, MapPipe, MapToPipe, MapValuesPipe, MarkdownDirective, MarkdownService, MarkdownTestPage, MarkdownTestingModule, MarkdownUtils, MaskitoPlaceholderPipe, MaskitoTestPage, MatAutocompleteConfigHolder, MatAutocompleteField, MatAutocompleteFieldUtils, MatBadgeTestPage, MatBooleanField, MatChipsField, MatColorPipe, MatCommonTestPage, MatDate, MatDateShort, MatDateTime, MatDuration, MatLatLongField, MatLatLongFieldInput, MatPaginatorI18n, MatStepperI18n, MatSwipeField, MaterialAutocompleteFooterDirective, MaterialAutocompleteHeaderDirective, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageAttachment, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NativeWebSocket, NavActionsColumnComponent, NestedTableTestPage, NetworkService, NetworkUtils, NewTokenForm, NewTokenModal, NgInitDirective, NgVarDirective, NoHtmlPipe, NotEmptyArrayPipe, NumberFormatPipe, ObservableTestPage, OddPipe, OtherMenuTestingPage, PEER_URL_REGEXP, PLUS_PLACEHOLDER_CHAR_REGEXP_GLOBAL, PRINT_ID_QUERY_PARAM, PRINT_LOADING_STORAGE_KEY_PREFIX, PRIORITIZED_AUTHORITIES, PUBKEY_REGEXP, Peer, Person, PersonFilter, PersonFilterAdditionalFields, PersonFragments, PersonService, PersonToStringPipe, PersonUtils, PersonValidatorService, PlatformService, PrintService, ProgressBarService, ProgressInterceptor, PropertiesFormTestPage, PropertiesFormTestingModule, PropertyEntity, PropertyEntityFilter, PropertyEntityValidator, PropertyFormatPipe, PropertyGetPipe, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS, Referential, ReferentialFilter, ReferentialRef, ReferentialToStringPipe, ReferentialUtils, ReferentialValidatorService, ReferentialsToStringPipe, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, RxStateOutput, RxStateProperty, RxStateRegister, RxStateSelect, SCRYPT_PARAMS, SETTINGS_COMPACT_ROWS, SETTINGS_DISPLAY_COLUMNS, SETTINGS_FILTER, SETTINGS_PAGE_SIZE, SETTINGS_SORTED_COLUMN, SETTINGS_STORAGE_KEY, SETTINGS_TRANSIENT_PROPERTIES, SHARED_ICONS, SHARED_IONIC_COMPONENTS, SHARED_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_ICONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, STARTUP_DATA_STORAGE_KEY, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedIonicModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StartupService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StorageServiceWrapper, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, TRACKED_QUERIES_STORAGE_KEY, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingModule, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, TimeFormatPipe, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, TruncateHtmlPipe, USER_CONTROLLER, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserController, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, assignSkipUndefined, base64ArrayBuffer, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, collectByPropertyPath, compareValues, compareValuesDesc, compareVersionNumbers, composeComparators, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createApolloClientOptions, createAppStartupInitializer, createPromiseEvent, createPromiseEventEmitter, createTestingImageDataUrl, createTrackerLink, decorateWithTakeUntil, deepMergeSkipUndefined, departmentToString, departmentsToString, disableAndClearControl, disableAndClearControls, disableControl, disableControls, emitPromiseEvent, enableControl, enableControls, enableRxStateProdMode, entityToString, equals, equalsOrNil, escapeRegExp, expansionAnimation, fadeInAnimation, fadeInOutAnimation, fadeInSlowAnimation, filterFalse, filterFormErrors, filterFormErrorsByPath, filterFormErrorsByPrefix, filterNotNil, filterNumberInput, filterTrue, findParentWithClass, firstArrayValue, firstFalse, firstFalsePromise, firstNotNil, firstNotNilPromise, firstPromise, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorLuminance, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormControlName, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hasFocusWithin, hexToRgb, hexToRgbArray, initArrayControlsFromValues, initializeSharedModule, interpolateString, intersectArrays, isAndroid, isAppInputField, isAppTextInputField, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isFocusableElement, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isLightColor, isMacOS, isMobile, isMutationOperation, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isSubscriptionOperation, isTouchUi, isValidVersion, isVersionCompatible, isWebAnimationsSupported, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, loggerLink, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mergeObjectsWithoutUndefined, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseBooleanText, parseLatLongText, parseLatitudeOrLongitude, parseVersion, propertiesPathComparator, propertyComparator, propertyPathComparator, provideAccountService, provideAllIonicIcons, provideIonicIcons, provideIonicIconsInitializer, providePersonService, referentialToString, referentialsToString, registerIonicIcons, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, restoreTrackedQueries, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, setControlRequired, setControlsEnabled, setFormErrors, setPropertyByPath, setTabIndex, sleep, slideDownAnimation, slideInAnimation, slideInOutAnimation, slideUpDownAnimation, sort, splitArrayInChunks, splitById, splitByProperty, splitDegreesToDDArray, splitDegreesToDDMMArray, splitDegreesToDDMMSSArray, startsWithUpperCase, suggestFromArray, suggestFromStringArray, tabindexComparator, testUserAgent, toBoolean, toDateISOString, toDuration, toFloat, toInt, toLoadData, toLoadResult, toNotNil, toNumber, trimEmptyToNull, truncateHtml, typeTextIntoInput, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
58845
+ export { ADMIN_ICONS, APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_ACCOUNT_SERVICE, APP_ACCOUNT_SERVICE_OPTIONS, APP_CELL_SELECTION_SERVICE_CONFIG_TOKEN, APP_CELL_SELECTION_SERVICE_TOKEN, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FILE_TRANSFER_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_SERVICE, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOME_TOOLBAR_BUTTONS, APP_HOTKEYS_CONFIG, APP_INPUT_FIELD, APP_JOB_PROGRESSION_SERVICE, APP_LOCALES, APP_LOCAL_SETTINGS, APP_LOCAL_SETTINGS_OPTIONS, APP_LOCAL_STORAGE_TYPE_POLICIES, APP_LOGGING_SERVICE, APP_MENU_ITEMS, APP_MENU_OPTIONS, APP_MESSAGE_SERVICE_OPTIONS, APP_NAMED_FILTER_SERVICE, APP_PERSON_SERVICE, APP_PERSON_SERVICE_OPTIONS, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_SHOW_TOOLTIP, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TABLE_OPTIONS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, AbstractPersonService, AbstractSelectionModelPipe, AbstractTableSelectionPipe, AbstractUserEventService, Account, AccountPage, AccountService, AccountToStringPipe, AccountUtils, ActionsColumnComponent, AdminModule, AdminRoutingModule, AdminUsersModule, Alerts, AndroidOsEnvironment, AppAboutModalModule, AppAccountModule, AppAsyncTable, AppAuthForm, AppAuthModal, AppAuthModule, AppChangePasswordModule, AppChangePasswordPage, AppEditor, AppEditorOptions, AppEntityEditor, AppEntityEditorModal, AppEntityEditorModalOptions, AppEntityFormModule, AppForm, AppFormArray, AppFormButtonsBarModule, AppFormContainer, AppFormField, AppFormModule, AppFormProvider, AppFormUtils, AppGestureConfig, AppHomePageModule, AppIconComponent, AppIconModule, AppIconSelectorField, AppIconSelectorModal, AppIconSelectorModule, AppImageGalleryComponent, AppImageGallerySlideshowComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSelectUsersModal, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextFormModule, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppWebSocket, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFindPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayMapPipe, ArrayPluckPipe, ArraySlicePipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutoTooltipDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_ICONS, CORE_TESTING_PAGES, CapitalizePipe, CellIdentifierDirective, CellSelectionDirective, CellSelectionService, CellValueChangeListener, ChangeCaseToUnderscorePipe, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, DATE_ISO_PATTERNS, DATE_MATCH_REGEXP, DATE_PATTERN, DATE_UNIX_MS_TIMESTAMP, DATE_UNIX_TIMESTAMP, DEFAULT_JOIN_ARRAY_VALUES_SEPARATOR, DEFAULT_JOIN_PROPERTIES_SEPARATOR, DEFAULT_MENU_SHOW_WHEN, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLACEHOLDER_CHAR, DEFAULT_REQUIRED_COLUMNS, DateDiffDurationPipe, DateFormatPipe, DateFormatService, DateFromNowPipe, DateFromPipe, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EmptyMissingTranslationHandler, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FILE_KIND_ICONS, FeedAttachmentUtils, FeedDirective, FeedModule, FeedPage, FeedService, FeedsComponent, FileAttachmentComponent, FileAttachmentPreviewModal, FileAttachmentUtils, FileDownloadService, FileResponse, FileService, FileSizePipe, FileTypeUtils, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayAtControlPipe, FormArrayAtGroupPipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetNamePipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GitlabUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, HttpUtils, IMAGE_DEFAULTS, ION_ICONS, IPosition, IconSelectorTestPage, IconSelectorTestingModule, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImageSizes, ImagesUtils, InMemoryEntitiesService, InputFieldTestPage, IsAllSelectedPipe, IsEmptySelectionPipe, IsLoginAccountPipe, IsMultipleSelectionPipe, IsNilOrBlankPipe, IsNilOrNaNPipe, IsNilPipe, IsNotAllSelectedPipe, IsNotEmptySelectionPipe, IsNotNilOrBlankPipe, IsNotNilOrNaNPipe, IsNotNilPipe, IsOnDeskPipe, IsOnFieldPipe, IsSelectedPipe, IsSingleSelectionPipe, IsValidDatePipe, JobModule, JobProgression, JobProgressionComponent, JobProgressionIcon, JobProgressionList, JobProgressionService, JobProgressionTestService, JobProgressionTestingPage, JobTestingModule, JobUtils, JsonFeedUtils, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, ListFormTestPage, ListFormTestingModule, LocalSettingsService, LogLevel, LogUtils, Logger, LoggingService, LoggingServiceModule, LongitudeFormatPipe, MASKS, MASK_RANGES, MAT_FORM_FIELD_DEFAULT_APPEARANCE, MAT_FORM_FIELD_DEFAULT_SUBSCRIPT_SIZING, MINIFY_ENTITY_FOR_LOCAL_STORAGE, MINIFY_ENTITY_FOR_POD, MOMENT_NO_TIME_PROPERTY, MapGetPipe, MapKeysPipe, MapPipe, MapToPipe, MapValuesPipe, MarkdownDirective, MarkdownService, MarkdownTestPage, MarkdownTestingModule, MarkdownUtils, MaskitoPlaceholderPipe, MaskitoTestPage, MatAutocompleteConfigHolder, MatAutocompleteField, MatAutocompleteFieldUtils, MatBadgeTestPage, MatBooleanField, MatChipsField, MatColorPipe, MatCommonTestPage, MatDate, MatDateShort, MatDateTime, MatDuration, MatLatLongField, MatLatLongFieldInput, MatPaginatorI18n, MatStepperI18n, MatSwipeField, MaterialAutocompleteFooterDirective, MaterialAutocompleteHeaderDirective, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageAttachment, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NativeWebSocket, NavActionsColumnComponent, NestedTableTestPage, NetworkService, NetworkUtils, NewTokenForm, NewTokenModal, NgInitDirective, NgVarDirective, NoHtmlPipe, NotEmptyArrayPipe, NumberFormatPipe, ObservableTestPage, OddPipe, OtherMenuTestingPage, PEER_URL_REGEXP, PLUS_PLACEHOLDER_CHAR_REGEXP_GLOBAL, PRINT_ID_QUERY_PARAM, PRINT_LOADING_STORAGE_KEY_PREFIX, PRIORITIZED_AUTHORITIES, PUBKEY_REGEXP, Peer, Person, PersonFilter, PersonFilterAdditionalFields, PersonFragments, PersonService, PersonToStringPipe, PersonUtils, PersonValidatorService, PlatformService, PrintService, ProgressBarService, ProgressInterceptor, PropertiesFormTestPage, PropertiesFormTestingModule, PropertyEntity, PropertyEntityFilter, PropertyEntityValidator, PropertyFormatPipe, PropertyGetPipe, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS, Referential, ReferentialFilter, ReferentialRef, ReferentialToStringPipe, ReferentialUtils, ReferentialValidatorService, ReferentialsToStringPipe, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, RxStateOutput, RxStateProperty, RxStateRegister, RxStateSelect, SCRYPT_PARAMS, SETTINGS_COMPACT_ROWS, SETTINGS_DISPLAY_COLUMNS, SETTINGS_FILTER, SETTINGS_PAGE_SIZE, SETTINGS_SORTED_COLUMN, SETTINGS_STORAGE_KEY, SETTINGS_TRANSIENT_PROPERTIES, SHARED_ICONS, SHARED_IONIC_COMPONENTS, SHARED_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_ICONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, STARTUP_DATA_STORAGE_KEY, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFileAttachmentModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedIonicModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StartupService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StorageServiceWrapper, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, TRACKED_QUERIES_STORAGE_KEY, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingModule, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, TimeFormatPipe, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, TruncateHtmlPipe, USER_CONTROLLER, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserController, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, assignSkipUndefined, base64ArrayBuffer, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, collectByPropertyPath, compareValues, compareValuesDesc, compareVersionNumbers, composeComparators, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createApolloClientOptions, createAppStartupInitializer, createPromiseEvent, createPromiseEventEmitter, createTestingImageDataUrl, createTrackerLink, decorateWithTakeUntil, deepMergeSkipUndefined, departmentToString, departmentsToString, disableAndClearControl, disableAndClearControls, disableControl, disableControls, emitPromiseEvent, enableControl, enableControls, enableRxStateProdMode, entityToString, equals, equalsOrNil, escapeRegExp, expansionAnimation, fadeInAnimation, fadeInOutAnimation, fadeInSlowAnimation, filterFalse, filterFormErrors, filterFormErrorsByPath, filterFormErrorsByPrefix, filterNotNil, filterNumberInput, filterTrue, findParentWithClass, firstArrayValue, firstFalse, firstFalsePromise, firstNotNil, firstNotNilPromise, firstPromise, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorLuminance, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormControlName, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hasFocusWithin, hexToRgb, hexToRgbArray, initArrayControlsFromValues, initializeSharedModule, interpolateString, intersectArrays, isAndroid, isAppInputField, isAppTextInputField, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isFocusableElement, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isLightColor, isMacOS, isMobile, isMutationOperation, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isSubscriptionOperation, isTouchUi, isValidVersion, isVersionCompatible, isWebAnimationsSupported, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, loggerLink, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mergeObjectsWithoutUndefined, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseBooleanText, parseLatLongText, parseLatitudeOrLongitude, parseVersion, propertiesPathComparator, propertyComparator, propertyPathComparator, provideAccountService, provideAllIonicIcons, provideIonicIcons, provideIonicIconsInitializer, providePersonService, referentialToString, referentialsToString, registerIonicIcons, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, restoreTrackedQueries, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, setControlRequired, setControlsEnabled, setFormErrors, setPropertyByPath, setTabIndex, sleep, slideDownAnimation, slideInAnimation, slideInOutAnimation, slideUpDownAnimation, sort, splitArrayInChunks, splitById, splitByProperty, splitDegreesToDDArray, splitDegreesToDDMMArray, splitDegreesToDDMMSSArray, startsWithUpperCase, suggestFromArray, suggestFromStringArray, tabindexComparator, testUserAgent, toBoolean, toDateISOString, toDuration, toFloat, toInt, toLoadData, toLoadResult, toNotNil, toNumber, trimEmptyToNull, truncateHtml, typeTextIntoInput, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
58770
58846
  //# sourceMappingURL=sumaris-net.ngx-components.mjs.map