@sumaris-net/ngx-components 18.13.0 → 18.14.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.
Files changed (38) hide show
  1. package/doc/changelog.md +4 -0
  2. package/doc/feed/feed-en.json +26 -0
  3. package/doc/feed/feed-fr.json +26 -0
  4. package/esm2022/public_api.mjs +9 -1
  5. package/esm2022/src/app/core/home/home.mjs +13 -12
  6. package/esm2022/src/app/core/home/home.module.mjs +7 -3
  7. package/esm2022/src/app/core/services/local-settings.service.mjs +3 -2
  8. package/esm2022/src/app/core/services/network.service.mjs +12 -1
  9. package/esm2022/src/app/social/feed/discourse/discourse-feed.service.mjs +179 -0
  10. package/esm2022/src/app/social/feed/discourse/discourse.model.mjs +2 -0
  11. package/esm2022/src/app/social/feed/discourse/discourse.utils.mjs +168 -0
  12. package/esm2022/src/app/social/feed/feed.component.mjs +121 -0
  13. package/esm2022/src/app/social/feed/feed.directive.mjs +151 -0
  14. package/esm2022/src/app/social/feed/feed.model.mjs +68 -0
  15. package/esm2022/src/app/social/feed/feed.module.mjs +22 -0
  16. package/esm2022/src/app/social/feed/feed.service.mjs +3 -0
  17. package/esm2022/src/environments/environment.class.mjs +2 -1
  18. package/esm2022/src/environments/environment.mjs +10 -1
  19. package/fesm2022/sumaris-net.ngx-components.mjs +681 -8
  20. package/fesm2022/sumaris-net.ngx-components.mjs.map +1 -1
  21. package/package.json +1 -1
  22. package/public_api.d.ts +8 -0
  23. package/src/app/core/home/home.d.ts +1 -1
  24. package/src/app/core/home/home.module.d.ts +2 -1
  25. package/src/app/core/services/local-settings.service.d.ts +1 -0
  26. package/src/app/core/services/network.service.d.ts +1 -0
  27. package/src/app/social/feed/discourse/discourse-feed.service.d.ts +55 -0
  28. package/src/app/social/feed/discourse/discourse.model.d.ts +7 -0
  29. package/src/app/social/feed/discourse/discourse.utils.d.ts +44 -0
  30. package/src/app/social/feed/feed.component.d.ts +39 -0
  31. package/src/app/social/feed/feed.directive.d.ts +32 -0
  32. package/src/app/social/feed/feed.model.d.ts +69 -0
  33. package/src/app/social/feed/feed.module.d.ts +11 -0
  34. package/src/app/social/feed/feed.service.d.ts +13 -0
  35. package/src/assets/i18n/en-US.json +5 -1
  36. package/src/assets/i18n/en.json +5 -0
  37. package/src/assets/i18n/fr.json +5 -0
  38. package/src/environments/environment.class.d.ts +8 -0
@@ -4,7 +4,7 @@ import { firstValueFrom, shareReplay, tap, of, timer, Subject, merge, delay, isO
4
4
  import { catchError, map, filter, takeUntil, first, switchMap, tap as tap$1, throttleTime, debounceTime, startWith, distinctUntilChanged, mergeMap, skip, finalize, bufferWhen, distinctUntilKeyChanged, take } from 'rxjs/operators';
5
5
  import * as cloneImported from 'clone';
6
6
  import * as i3$1 from '@angular/common';
7
- import { CommonModule, DOCUMENT, LocationStrategy, Location, AsyncPipe, JsonPipe } from '@angular/common';
7
+ import { CommonModule, DOCUMENT, LocationStrategy, NgOptimizedImage, Location, AsyncPipe, JsonPipe } from '@angular/common';
8
8
  import { CdkTableModule } from '@angular/cdk/table';
9
9
  import * as i1$5 from '@angular/cdk/a11y';
10
10
  import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
@@ -155,6 +155,8 @@ import { Camera, CameraResultType } from '@capacitor/camera';
155
155
  import { Geolocation } from '@capacitor/geolocation';
156
156
  import * as i1$8 from '@e-is/ngx-material-table';
157
157
  import { ValidatorService, TableDataSource, AsyncTableDataSource } from '@e-is/ngx-material-table';
158
+ import DiscourseAPI from 'discourse2';
159
+ import { Promise as Promise$1 } from '@rx-angular/cdk/zone-less/browser';
158
160
  import 'moment-timezone';
159
161
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
160
162
 
@@ -220,6 +222,7 @@ class Environment {
220
222
  // Account
221
223
  account;
222
224
  entityEditor;
225
+ feed;
223
226
  }
224
227
 
225
228
  const DATE_ISO_PATTERN = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
@@ -12892,6 +12895,15 @@ const environment = Object.freeze({
12892
12895
  enableListenChanges: true,
12893
12896
  listenIntervalInSeconds: 60,
12894
12897
  },
12898
+ feed: {
12899
+ jsonFeed: {
12900
+ fr: ['https://gitlab.ifremer.fr/sih-public/sumaris/ngx-sumaris-components/-/raw/master/doc/feed/feed-fr.json'],
12901
+ en: ['https://gitlab.ifremer.fr/sih-public/sumaris/ngx-sumaris-components/-/raw/master/doc/feed/feed-en.json'],
12902
+ },
12903
+ maxContentLength: 1000,
12904
+ maxAgeInMonths: -1,
12905
+ maxCount: 3,
12906
+ },
12895
12907
  });
12896
12908
 
12897
12909
  class EntityClasses {
@@ -13243,6 +13255,7 @@ class LocalSettingsService extends StartableService {
13243
13255
  defaultSettings;
13244
13256
  onChange = new Subject();
13245
13257
  darkMode$ = merge(this.onChange, this.startSubject).pipe(distinctUntilChanged(), map((data) => data?.darkMode || false));
13258
+ locale$ = merge(this.onChange, this.startSubject).pipe(startWith(this.settings), distinctUntilChanged(), map((data) => data?.locale || this.environment?.defaultLocale));
13246
13259
  _optionDefs;
13247
13260
  _serializeAsString;
13248
13261
  _$persist;
@@ -19765,6 +19778,17 @@ class NetworkService extends StartableObservableService {
19765
19778
  console.debug(`[network] All cache cleared, in ${Date.now() - now}ms`);
19766
19779
  });
19767
19780
  }
19781
+ async fetch(url) {
19782
+ if (!url)
19783
+ throw new Error('Missing url');
19784
+ try {
19785
+ return firstValueFrom(this.http.get(url));
19786
+ }
19787
+ catch (err) {
19788
+ console.error(`[network] Error fetching data from ${url}:`, err);
19789
+ return null;
19790
+ }
19791
+ }
19768
19792
  /* -- protected functions -- */
19769
19793
  async ngOnStart(peer) {
19770
19794
  console.info('[network] Starting network...');
@@ -33304,6 +33328,477 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
33304
33328
  args: ['showList']
33305
33329
  }] } });
33306
33330
 
33331
+ class JsonFeedUtils {
33332
+ static DEFAULT_MAX_POST_AGE_IN_MONTHS = 3;
33333
+ static VERSION_1 = 'https://jsonfeed.org/version/1';
33334
+ static VERSION_1_1 = 'https://jsonfeed.org/version/1.1';
33335
+ static ACCEPTED_VERSIONS = [this.VERSION_1, this.VERSION_1_1];
33336
+ static removeJsonExtension(url) {
33337
+ if (!url)
33338
+ return url;
33339
+ return url.replace(/\.json$/, '');
33340
+ }
33341
+ static isJsonFeed(json) {
33342
+ return (json && this.ACCEPTED_VERSIONS.includes(json.version) && isNotEmptyArray(json.items)) || isNotNilOrBlank(json.next_url);
33343
+ }
33344
+ static isJsonFeedItem(json) {
33345
+ return json && isNotEmptyArray(json.id) && (isNotNilOrBlank(json.content_text) || isNotNilOrBlank(json.content_html));
33346
+ }
33347
+ static truncateFeedItemsHtml(feed, opts) {
33348
+ if (!feed?.items)
33349
+ return feed;
33350
+ feed.items.forEach((item) => {
33351
+ // Truncate items html
33352
+ if (item.content_html) {
33353
+ const html = this.truncateHtml(item.content_html, opts);
33354
+ item.truncated = html.length !== item.content_html.length;
33355
+ item.content_html = html;
33356
+ }
33357
+ });
33358
+ return feed;
33359
+ }
33360
+ static truncateHtml(html, opts) {
33361
+ if (!html)
33362
+ return html;
33363
+ const maxContentLength = opts?.maxContentLength ?? 0;
33364
+ if (maxContentLength > 0 && html?.length > maxContentLength) {
33365
+ const endIndex = Math.max(html.lastIndexOf(' ', maxContentLength), html.lastIndexOf('<', maxContentLength));
33366
+ return html.substring(0, endIndex) + ' (...)';
33367
+ }
33368
+ return html;
33369
+ }
33370
+ /**
33371
+ * Migrate from old version 1.0 to 1.1
33372
+ * @param feed
33373
+ */
33374
+ static migrateFeed(feed) {
33375
+ if (!feed)
33376
+ return feed;
33377
+ // Migrate from old version 1.0 to 1.1
33378
+ if (feed.version === JsonFeedUtils.VERSION_1) {
33379
+ // Migrate feed author
33380
+ if (feed.author && !feed.authors) {
33381
+ feed.authors = [feed.author];
33382
+ delete feed.author;
33383
+ }
33384
+ // Migrate items author
33385
+ feed.items?.forEach((item) => {
33386
+ if (item.author && !item.authors) {
33387
+ item.authors = [item.author];
33388
+ delete item.author;
33389
+ }
33390
+ });
33391
+ // Change version
33392
+ feed.version = JsonFeedUtils.VERSION_1_1;
33393
+ }
33394
+ return feed;
33395
+ }
33396
+ }
33397
+
33398
+ class DiscourseUtils {
33399
+ static _logPrefix = '[discourse-utils]';
33400
+ static removeJsonExtension = JsonFeedUtils.removeJsonExtension;
33401
+ static isDiscourseObject(json) {
33402
+ return this.isDiscourseTopic(json) || this.isDiscourseCategory(json);
33403
+ }
33404
+ static isDiscourseTopic(json) {
33405
+ return (json && !!json.post_stream?.posts && json.fancy_title && json.slug && json.details?.created_by && true) || false;
33406
+ }
33407
+ static isDiscourseCategory(json) {
33408
+ return (json && json.topic_list?.topics && json.posters && true) || false;
33409
+ }
33410
+ static getDiscourseBaseUrl(url) {
33411
+ return UrlUtils.getRootUrl(url);
33412
+ }
33413
+ static isDiscourseCategoryUrl(url) {
33414
+ const baseUrl = this.getDiscourseBaseUrl(url);
33415
+ return baseUrl && url.substring(baseUrl.length).match(/^\/c\//);
33416
+ }
33417
+ static extractCategoryIdFromUrl(url) {
33418
+ const match = this.removeJsonExtension(url).match(/\/(\d+)$/);
33419
+ return match ? parseInt(match[1], 10) : null;
33420
+ }
33421
+ static extractCategorySlugFromUrl(url) {
33422
+ const match = this.removeJsonExtension(url).match(/\/c\/([\w/-]+)\/\d+$/);
33423
+ return match ? match[1] : null;
33424
+ }
33425
+ static extractTopicIdFromUrl(url) {
33426
+ const match = url.match(/\/(\d+)(?:$|\/)/) || url.match(/\/(\d+)\.json$/);
33427
+ return match ? parseInt(match[1], 10) : null;
33428
+ }
33429
+ static getLanguageFromTitle(title) {
33430
+ if (!title)
33431
+ return '';
33432
+ const match = title.match('\\s\\(([a-z]{2}(?:-[A-Z]{2})?)\\)$');
33433
+ return match && match[1];
33434
+ }
33435
+ static async getFeedsByUrl(url, opts) {
33436
+ try {
33437
+ // Check a category
33438
+ if (DiscourseUtils.isDiscourseCategoryUrl(url)) {
33439
+ console.debug(`${this._logPrefix}Detected a Discourse category at: ${url}`);
33440
+ return this.getFeedsByCategoryUrl(url, opts);
33441
+ }
33442
+ // Or a topic
33443
+ else {
33444
+ const post = await this.getFeedByTopicUrl(url, opts);
33445
+ return post ? [post] : [];
33446
+ }
33447
+ }
33448
+ catch (err) {
33449
+ console.error(`${this._logPrefix}`, err);
33450
+ return [];
33451
+ }
33452
+ }
33453
+ static async getFeedsByCategoryUrl(url, opts) {
33454
+ const categoryId = this.extractCategoryIdFromUrl(url);
33455
+ const categorySlug = this.extractCategorySlugFromUrl(url);
33456
+ const baseUrl = this.getDiscourseBaseUrl(url);
33457
+ const api = opts?.api ?? new DiscourseAPI(baseUrl);
33458
+ const category = await api.listCategoryTopics({ id: categoryId, slug: categorySlug });
33459
+ const topics = (category?.topic_list?.topics || []).filter((topic) => {
33460
+ const topicLocale = this.getLanguageFromTitle(topic.fancy_title);
33461
+ return (topic.pinned &&
33462
+ topic.visible &&
33463
+ // Keep same locale, if exists
33464
+ (!topicLocale || !opts?.locale || topicLocale === opts.locale));
33465
+ });
33466
+ const posts = (await Promise$1.all(topics.map(async (t) => {
33467
+ const topic = await api.getTopic({ id: t.id.toString() });
33468
+ // Ignore 'About' topics
33469
+ if (topic.tags.includes('about-category'))
33470
+ return undefined;
33471
+ const posts = this.filterRecentPosts(topic.post_stream.posts, opts);
33472
+ if (isEmptyArray(posts))
33473
+ return undefined;
33474
+ return this.toFeed(posts, topic, url, opts);
33475
+ }))).filter(isNotNil);
33476
+ if (isEmptyArray(posts)) {
33477
+ console.warn(`${this._logPrefix}No recent post found on category #${categoryId}`);
33478
+ }
33479
+ return posts;
33480
+ }
33481
+ static async getFeedByTopicUrl(url, opts) {
33482
+ const topicId = this.extractTopicIdFromUrl(url);
33483
+ if (!topicId) {
33484
+ console.error(`${this._logPrefix}Invalid URL`);
33485
+ return undefined;
33486
+ }
33487
+ const api = opts?.api ?? new DiscourseAPI(this.getDiscourseBaseUrl(url));
33488
+ const topic = await api.getTopic({ id: topicId.toString() });
33489
+ // Ignore about category
33490
+ if (topic.tags?.includes('about-category'))
33491
+ return undefined;
33492
+ const posts = this.filterRecentPosts(topic.post_stream.posts, opts);
33493
+ if (isEmptyArray(posts)) {
33494
+ console.warn(`${this._logPrefix}No recent posts found in topic #${topicId}`);
33495
+ return undefined;
33496
+ }
33497
+ return this.toFeed(posts, topic, url, opts);
33498
+ }
33499
+ static filterRecentPosts(posts, opts) {
33500
+ const maxAgeInMonths = opts?.maxAgeInMonths ?? JsonFeedUtils.DEFAULT_MAX_POST_AGE_IN_MONTHS;
33501
+ if (maxAgeInMonths <= 0)
33502
+ return posts; // Accept all
33503
+ const minDate = maxAgeInMonths > 0 ? DateUtils.moment().subtract(maxAgeInMonths, 'month').startOf('day').utc() : undefined;
33504
+ return (posts || []).filter((post) => post && DateUtils.moment(post.created_at).isSameOrAfter(minDate));
33505
+ }
33506
+ static toFeed(posts, topic, url, opts) {
33507
+ const baseUrl = this.getDiscourseBaseUrl(url);
33508
+ const title = this.cleanTitle(topic.fancy_title ?? topic.title);
33509
+ const items = (posts || []).map((post) => this.toFeedItem(post, topic, url, opts));
33510
+ const firstItem = items?.[0];
33511
+ if (firstItem)
33512
+ firstItem.title = title;
33513
+ const authorName = topic?.details?.created_by && (topic.details.created_by.name || topic.details.created_by.username);
33514
+ const authorAvatar = baseUrl + topic.details.created_by.avatar_template.replace('{size}', '48');
33515
+ return {
33516
+ version: JsonFeedUtils.VERSION_1_1,
33517
+ feed_url: url,
33518
+ title: this.cleanTitle(topic.fancy_title),
33519
+ expired: topic.archived,
33520
+ authors: [
33521
+ {
33522
+ name: authorName,
33523
+ avatar: authorAvatar,
33524
+ },
33525
+ ],
33526
+ items,
33527
+ tag_template: baseUrl + '/tag/{tag}',
33528
+ };
33529
+ }
33530
+ static toFeedItem(post, topic, url, opts) {
33531
+ const baseUrl = this.getDiscourseBaseUrl(url);
33532
+ url = post.post_url ? baseUrl + post.post_url : url;
33533
+ const id = post.post_url ? baseUrl + post.post_url : post.id;
33534
+ const html = JsonFeedUtils.truncateHtml(post.cooked, opts);
33535
+ const truncated = html?.length !== post.cooked?.length;
33536
+ const authorName = post.name || post.username;
33537
+ const authorAvatar = baseUrl + (post.avatar_template.replace('{size}', '48') ?? topic.details.last_poster.avatar_template.replace('{size}', '48'));
33538
+ return {
33539
+ id,
33540
+ url,
33541
+ authors: [
33542
+ {
33543
+ name: authorName,
33544
+ avatar: authorAvatar,
33545
+ },
33546
+ ],
33547
+ content_html: html,
33548
+ date_published: post.created_at,
33549
+ tags: topic.tags,
33550
+ truncated,
33551
+ };
33552
+ }
33553
+ static cleanTitle(title) {
33554
+ if (!title)
33555
+ return undefined;
33556
+ return title.replace(/\s\([a-z]{2}(:?-[A-Z]{2})?\)$/, '');
33557
+ }
33558
+ }
33559
+
33560
+ const APP_FEED_SERVICE = new InjectionToken('FeeService');
33561
+
33562
+ class FeedDirective {
33563
+ element;
33564
+ _subscription = new Subscription();
33565
+ platform = inject(PlatformService);
33566
+ locationStrategy = inject(LocationStrategy);
33567
+ navController = inject(NavController);
33568
+ router = inject(Router);
33569
+ route = inject(ActivatedRoute);
33570
+ _feedUrl;
33571
+ _baseUrl;
33572
+ set feedUrl(value) {
33573
+ this._feedUrl = value;
33574
+ this._baseUrl = value && DiscourseUtils.removeJsonExtension(value);
33575
+ }
33576
+ get feedUrl() {
33577
+ return this._feedUrl;
33578
+ }
33579
+ constructor(element) {
33580
+ this.element = element;
33581
+ // DEBUG
33582
+ //console.debug('[feed-directive] Creating feed directive');
33583
+ }
33584
+ ngAfterViewInit() {
33585
+ console.debug('[feed-directive] Processing anchors');
33586
+ // Listening click events
33587
+ this._subscription.add(this.listenClickEvents(this.element.nativeElement));
33588
+ }
33589
+ ngOnDestroy() {
33590
+ this._subscription.unsubscribe();
33591
+ }
33592
+ listenClickEvents(nativeElement) {
33593
+ console.debug('[feed-directive] Start listening click events...');
33594
+ const subscription = new Subscription();
33595
+ const listener = (event) => this.click(event, nativeElement);
33596
+ const links = nativeElement.querySelectorAll('a');
33597
+ links.forEach((link) => {
33598
+ // DEBUG
33599
+ //console.debug('[feed-directive] Adding click listener to', link);
33600
+ link.addEventListener('click', listener);
33601
+ subscription.add(() => link.removeEventListener('click', listener));
33602
+ });
33603
+ // DEBUG
33604
+ subscription.add(() => console.debug('[feed-directive] Stop listening click events'));
33605
+ return subscription;
33606
+ }
33607
+ async click(event, containerElement) {
33608
+ console.debug('[feed-directive] Processing click event...', event);
33609
+ if (!(event.target instanceof HTMLAnchorElement) && !(event.target?.['tagName'] === 'A')) {
33610
+ console.warn('[feed-directive] Invalid click event target. Should an anchor <a>', event.target);
33611
+ return;
33612
+ }
33613
+ const element = event.target;
33614
+ let href = element.getAttribute('href');
33615
+ const fragment = UrlUtils.getFragment(href);
33616
+ // Fragment
33617
+ if (href?.startsWith('#') && isNotNilOrBlank(fragment)) {
33618
+ event.preventDefault();
33619
+ this.scrollToAnchor(containerElement, fragment);
33620
+ return true;
33621
+ }
33622
+ const routerLink = element.getAttribute('routerLink');
33623
+ if (isNotNilOrBlank(routerLink)) {
33624
+ event.preventDefault();
33625
+ return this.navController.navigateForward(routerLink);
33626
+ }
33627
+ // Resolve relative URL, if baseUrl has been set
33628
+ if (this._baseUrl && UrlUtils.isRelativeUrl(href)) {
33629
+ // Resolve URL, then open using the platform
33630
+ href = UrlUtils.resolveRelativeUrl(this._baseUrl, href);
33631
+ }
33632
+ // Resolve internal URL as an app route
33633
+ if (UrlUtils.isInternalUrl(href)) {
33634
+ const routePath = this.normalizeUrl(href.startsWith('/') ? href : `/${href}`);
33635
+ const urlTree = this.getUrlTree(routePath);
33636
+ event.preventDefault();
33637
+ this.router.navigated = false;
33638
+ // Opening route
33639
+ return this.navController.navigateForward(urlTree);
33640
+ }
33641
+ // External URL: open using the platform
33642
+ event.preventDefault();
33643
+ await this.platform.open(href);
33644
+ return true;
33645
+ }
33646
+ scrollToAnchor(containerElement, anchorId) {
33647
+ if (!containerElement || !anchorId) {
33648
+ console.warn("Missing 'containerElement' or 'fragment' argument.");
33649
+ return;
33650
+ }
33651
+ console.debug('[feed-directive-anchor] Scrolling to anchor #' + anchorId);
33652
+ // Find the target element with the specified fragment ID
33653
+ const targetElement = containerElement.querySelector(`#${anchorId}`);
33654
+ if (targetElement) {
33655
+ // Scroll smoothly to the target element
33656
+ // eslint-disable-next-line @rx-angular/prefer-no-layout-sensitive-apis
33657
+ targetElement.scrollIntoView({
33658
+ behavior: 'smooth', // Smooth scrolling behavior
33659
+ block: 'start', // Align the target element to the top of the container
33660
+ });
33661
+ }
33662
+ else {
33663
+ console.warn(`No element found with the ID "${anchorId}" inside the container.`);
33664
+ }
33665
+ }
33666
+ /**
33667
+ * Transform a relative URL to its absolute representation according to current router state.
33668
+ * @param url Relative URL path.
33669
+ * @return Absolute URL based on the current route.
33670
+ */
33671
+ normalizeUrl(url) {
33672
+ if (UrlUtils.isExternalUrl(url)) {
33673
+ return url;
33674
+ }
33675
+ if (this._baseUrl && UrlUtils.isRelativeUrl(url)) {
33676
+ return UrlUtils.resolveRelativeUrl(this._baseUrl, url);
33677
+ }
33678
+ const urlTree = this.getUrlTree(url);
33679
+ const serializedUrl = this.router.serializeUrl(urlTree);
33680
+ return this.locationStrategy.prepareExternalUrl(serializedUrl);
33681
+ }
33682
+ getUrlTree(url) {
33683
+ url = UrlUtils.normalizeUrl(url);
33684
+ const urlPath = UrlUtils.stripFragmentAndQuery(url) || UrlUtils.stripFragmentAndQuery(this.router.url);
33685
+ const parsedUrl = this.router.parseUrl(url);
33686
+ const fragment = parsedUrl.fragment;
33687
+ const queryParams = parsedUrl.queryParams;
33688
+ return this.router.createUrlTree([urlPath], { relativeTo: this.route, fragment, queryParams });
33689
+ }
33690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
33691
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: FeedDirective, selector: "feed,[feed]", inputs: { feedUrl: ["feed", "feedUrl"] }, ngImport: i0 });
33692
+ }
33693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedDirective, decorators: [{
33694
+ type: Directive,
33695
+ args: [{
33696
+ selector: 'feed,[feed]',
33697
+ }]
33698
+ }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { feedUrl: [{
33699
+ type: Input,
33700
+ args: ['feed']
33701
+ }] } });
33702
+
33703
+ class FeedsComponent {
33704
+ environment;
33705
+ feedService;
33706
+ _debug = false;
33707
+ _state = new RxState();
33708
+ translate = inject(TranslateService);
33709
+ platform = inject(PlatformService);
33710
+ router = inject(Router);
33711
+ version;
33712
+ feeds$;
33713
+ hasFeeds$;
33714
+ feeds;
33715
+ hasFeeds;
33716
+ showHeader = true;
33717
+ headerTextColor = 'light';
33718
+ constructor(environment, feedService) {
33719
+ this.environment = environment;
33720
+ this.feedService = feedService;
33721
+ this._state.connect('hasFeeds', this.feeds$.pipe(map(isNotEmptyArray)));
33722
+ if (this.feedService) {
33723
+ this._state.connect('feeds', this._state.select(['maxAgeInMonths', 'maxContentLength']).pipe(switchMap(({ maxAgeInMonths, maxContentLength }) => this.feedService.watchAll({
33724
+ maxAgeInMonths,
33725
+ maxContentLength,
33726
+ })), map(({ data }) => data)));
33727
+ }
33728
+ // Initial state
33729
+ this._state.set({
33730
+ maxAgeInMonths: this.environment.feed?.maxAgeInMonths ?? -1 /* all */,
33731
+ maxContentLength: this.environment.feed?.maxContentLength ?? -1 /* no max */,
33732
+ });
33733
+ // DEBUG
33734
+ this._debug = !environment.production;
33735
+ }
33736
+ openFeedHome(feed) {
33737
+ if (!feed) {
33738
+ this.feeds?.forEach((f) => this.openFeedHome(f));
33739
+ return;
33740
+ }
33741
+ const url = feed.home_page_url ?? DiscourseUtils.removeJsonExtension(feed.feed_url) ?? this.feedService.getHomeUrl();
33742
+ if (!url)
33743
+ return;
33744
+ return this.platform.open(url);
33745
+ }
33746
+ openUrl(url) {
33747
+ if (!url)
33748
+ return;
33749
+ const fixedUrl = DiscourseUtils.removeJsonExtension(url);
33750
+ return this.platform.open(fixedUrl);
33751
+ }
33752
+ openTag(feed, tag) {
33753
+ const url = this.feedService.getTagUrl(feed, tag);
33754
+ return this.openUrl(url);
33755
+ }
33756
+ getTags(feedOrItem) {
33757
+ if (JsonFeedUtils.isJsonFeed(feedOrItem)) {
33758
+ return arrayDistinct((feedOrItem.items || [])
33759
+ .map((item) => item.tags)
33760
+ .filter(isNotEmptyArray)
33761
+ .flat());
33762
+ }
33763
+ if (JsonFeedUtils.isJsonFeedItem(feedOrItem)) {
33764
+ return arrayDistinct((feedOrItem.tags || []).flat());
33765
+ }
33766
+ return [];
33767
+ }
33768
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedsComponent, deps: [{ token: ENVIRONMENT }, { token: APP_FEED_SERVICE, optional: true }], target: i0.ɵɵFactoryTarget.Component });
33769
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: FeedsComponent, selector: "app-feeds", inputs: { feeds: "feeds", showHeader: "showHeader", headerTextColor: "headerTextColor" }, providers: [RxState], ngImport: i0, template: "@if (feeds | isNotEmptyArray) {\n <div class=\"feed ion-padding-horizontal ion-no-padding ion-padding-top\">\n\n <!-- feeds -->\n @for (feed of feeds$ | push; track feed.feed_url) {\n\n <!-- top header -->\n @if (showHeader) {\n <ion-item lines=\"none\" color=\"secondary\">\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()\">\n <ion-label translate>SOCIAL.FEED.SHOW_ALL_FEED</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n\n <!-- items -->\n @for (item of feed.items; track item.id) {\n <ion-card>\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n\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 color=\"medium\">{{ 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)=\"openUrl(item.url || feed.feed_url)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <div 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>@if (!last) {\n &nbsp;\n }\n }\n </div>\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 [innerText]=\"item.content_text\"></p>\n }\n </ion-text>\n </ion-card-content>\n\n <ion-button (click)=\"openUrl(item.url)\" 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 </ion-card>\n }\n }\n </div>\n}\n", styles: [".feed ion-item{border-radius:12px 12px 0 0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}.feed ion-item ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed .tags{display:inline}.feed .tags a{color:var(--ion-color-step-600, #666666)!important}.feed ion-card{--ion-card-background: rgba(var(--ion-background-color-rgb), .6);--ion-card-color: var(--ion-text-color);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 16px}.feed ion-card ion-card-header ion-card-title{--color: var(--ion-text-color)}.feed ion-card ion-card-header ion-card-subtitle{--color: var(--ion-color-step-50)}.feed ion-card ion-card-header ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed ion-card ion-card-header ion-chip ion-avatar{border:1px solid var(--ion-color-primary)}.feed ion-card ion-card-header ion-chip ion-label{color:var(--color)}.feed ion-button{text-transform:unset;color:var(--ion-color-step-400)}.feed a:hover{color:var(--ion-color-medium)}\n"], dependencies: [{ kind: "component", type: i2$1.IonAvatar, selector: "ion-avatar" }, { kind: "component", type: i2$1.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$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$1.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonChip, selector: "ion-chip", inputs: ["color", "disabled", "mode", "outline"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: FeedDirective, selector: "feed,[feed]", inputs: ["feed"] }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: i19.RxPush, name: "push" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: MapPipe, name: "map" }] });
33770
+ }
33771
+ __decorate([
33772
+ RxStateSelect()
33773
+ ], FeedsComponent.prototype, "feeds$", void 0);
33774
+ __decorate([
33775
+ RxStateSelect()
33776
+ ], FeedsComponent.prototype, "hasFeeds$", void 0);
33777
+ __decorate([
33778
+ RxStateProperty()
33779
+ ], FeedsComponent.prototype, "feeds", void 0);
33780
+ __decorate([
33781
+ RxStateProperty()
33782
+ ], FeedsComponent.prototype, "hasFeeds", void 0);
33783
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedsComponent, decorators: [{
33784
+ type: Component,
33785
+ args: [{ selector: 'app-feeds', providers: [RxState], template: "@if (feeds | isNotEmptyArray) {\n <div class=\"feed ion-padding-horizontal ion-no-padding ion-padding-top\">\n\n <!-- feeds -->\n @for (feed of feeds$ | push; track feed.feed_url) {\n\n <!-- top header -->\n @if (showHeader) {\n <ion-item lines=\"none\" color=\"secondary\">\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()\">\n <ion-label translate>SOCIAL.FEED.SHOW_ALL_FEED</ion-label>\n <ion-icon slot=\"end\" name=\"chevron-forward-outline\"></ion-icon>\n </ion-button>\n </ion-item>\n }\n\n\n <!-- items -->\n @for (item of feed.items; track item.id) {\n <ion-card>\n <ion-card-header>\n <ion-card-subtitle style=\"vertical-align: middle\">\n\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 color=\"medium\">{{ 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)=\"openUrl(item.url || feed.feed_url)\" tappable>{{ item?.title }}</ion-card-title>\n\n <!-- tags -->\n @let tags = item | map: getTags;\n @if (tags | isNotEmptyArray) {\n <div 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>@if (!last) {\n &nbsp;\n }\n }\n </div>\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 [innerText]=\"item.content_text\"></p>\n }\n </ion-text>\n </ion-card-content>\n\n <ion-button (click)=\"openUrl(item.url)\" 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 </ion-card>\n }\n }\n </div>\n}\n", styles: [".feed ion-item{border-radius:12px 12px 0 0;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}.feed ion-item ion-icon[slot=start]{-webkit-margin-end:8px;margin-inline-end:8px}.feed .tags{display:inline}.feed .tags a{color:var(--ion-color-step-600, #666666)!important}.feed ion-card{--ion-card-background: rgba(var(--ion-background-color-rgb), .6);--ion-card-color: var(--ion-text-color);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin:0 0 16px}.feed ion-card ion-card-header ion-card-title{--color: var(--ion-text-color)}.feed ion-card ion-card-header ion-card-subtitle{--color: var(--ion-color-step-50)}.feed ion-card ion-card-header ion-chip{--background: transparent;--border-color: transparent;--border-width: 0}.feed ion-card ion-card-header ion-chip ion-avatar{border:1px solid var(--ion-color-primary)}.feed ion-card ion-card-header ion-chip ion-label{color:var(--color)}.feed ion-button{text-transform:unset;color:var(--ion-color-step-400)}.feed a:hover{color:var(--ion-color-medium)}\n"] }]
33786
+ }], ctorParameters: () => [{ type: Environment, decorators: [{
33787
+ type: Inject,
33788
+ args: [ENVIRONMENT]
33789
+ }] }, { type: undefined, decorators: [{
33790
+ type: Optional
33791
+ }, {
33792
+ type: Inject,
33793
+ args: [APP_FEED_SERVICE]
33794
+ }] }], propDecorators: { feeds$: [], hasFeeds$: [], feeds: [{
33795
+ type: Input
33796
+ }], hasFeeds: [], showHeader: [{
33797
+ type: Input
33798
+ }], headerTextColor: [{
33799
+ type: Input
33800
+ }] } });
33801
+
33307
33802
  /**
33308
33803
  * @param files
33309
33804
  * @deprecated use `getRandomImageWithCredit()` instead
@@ -33392,8 +33887,8 @@ class HomePage extends RxState {
33392
33887
  get darkMode() {
33393
33888
  return this.get('darkMode');
33394
33889
  }
33395
- get currentLocaleCode() {
33396
- return this.loading ? '' : (this.translate.currentLang || this.translate.defaultLang || this.environment.defaultLocale)?.substring(0, 2);
33890
+ get currentLocale() {
33891
+ return this.translate.currentLang || this.translate.defaultLang || this.environment.defaultLocale;
33397
33892
  }
33398
33893
  get partnerBannerSize() {
33399
33894
  return this.showLegalInformation ? 9 : 12;
@@ -33641,11 +34136,11 @@ class HomePage extends RxState {
33641
34136
  this.cd.markForCheck();
33642
34137
  }
33643
34138
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: HomePage, deps: [{ token: AccountService }, { token: i2$1.ModalController }, { token: i1$1.TranslateService }, { token: i2$1.ToastController }, { token: ConfigService }, { token: PlatformService }, { token: i0.ChangeDetectorRef }, { token: NetworkService }, { token: LocalSettingsService }, { token: ENVIRONMENT }, { token: APP_LOCALES }, { token: APP_HOME_BUTTONS, optional: true }, { token: APP_HOME_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Component });
33644
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: HomePage, selector: "app-page-home", usesInheritance: true, ngImport: i0, template: "<app-toolbar [canGoBack]=\"false\" visible-xs visible-sm visible-mobile>\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon [autoHide]=\"autoHideNotificationIcon\"></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button (click)=\"toggleDarkMode()\">\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'sunny' : 'moon'\"></ion-icon>\n </ion-button>\n }\n\n <!-- Change locale button -->\n <ion-button [matMenuTriggerFor]=\"localeMenu\">\n <ion-icon slot=\"start\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n</app-toolbar>\n\n<!-- Change locale menu -->\n<mat-menu #localeMenu=\"matMenu\">\n <ng-template matMenuContent>\n @for (item of locales; track item.key) {\n <button mat-menu-item (click)=\"changeLanguage(item.key)\">\n <ion-label>{{ item.value }}</ion-label>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<ion-content [ngStyle]=\"contentStyle\" no-padding-xs>\n @if (contentCredits | isNotNilOrBlank) {\n <span class=\"content-credits\">\n <ion-text>{{ contentCredits }}</ion-text>\n </span>\n }\n\n <!-- loading spinner -->\n <div class=\"loading-page\" [class.cdk-visually-hidden]=\"!(loading$ | push)\">\n @if (showSpinner) {\n <div class=\"spinner\">\n <p [innerHTML]=\"'COMMON.LOADING_DOTS' | translate\"></p>\n <div class=\"sk-cube1 sk-cube\"></div>\n <div class=\"sk-cube2 sk-cube\"></div>\n <div class=\"sk-cube4 sk-cube\"></div>\n <div class=\"sk-cube3 sk-cube\"></div>\n </div>\n }\n </div>\n\n <!-- Desktop: translucent top toolbar -->\n @if (!mobile) {\n <div class=\"hidden-xs hidden-sm\">\n <ion-toolbar color=\"traansparent\" translucent>\n <ion-buttons slot=\"start\">\n <ion-menu-toggle>\n <ion-button color=\"light\" fill=\"clear\">\n <ion-icon slot=\"icon-only\" name=\"menu\"></ion-icon>\n </ion-button>\n </ion-menu-toggle>\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon\n [style]=\"'ion-button'\"\n color=\"secondary\"\n unreadColor=\"tertiary\"\n fill=\"solid\"\n [autoHide]=\"autoHideNotificationIcon\"\n ></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n (click)=\"toggleDarkMode()\"\n [title]=\"'HOME.BTN_DARK_MODE' | translate\"\n >\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'moon' : 'sunny'\"></ion-icon>\n </ion-button>\n }\n <!-- change locale button -->\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n [matMenuTriggerFor]=\"localeMenu\"\n [title]=\"'SETTINGS.LOCALE' | translate\"\n >\n <ion-icon slot=\"icon-only\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </div>\n }\n\n <!-- Install (and upgrade) card -->\n <app-install-upgrade-card [isLogin]=\"isLogin\" [showInstallButton]=\"true\"></app-install-upgrade-card>\n\n @if (!(loading$ | push)) {\n <!-- Welcome card -->\n <ion-card class=\"main welcome ion-padding ion-text-center ion-align-self-center\" @fadeInAnimation>\n <ion-card-header>\n <ion-card-title class=\"ion-text-center\">\n <span *ngIf=\"isWeb\" [innerHTML]=\"'HOME.WELCOME_WEB' | translate: { appName: appName }\"></span>\n <span *ngIf=\"!isWeb\" [innerHTML]=\"'HOME.WELCOME_APP' | translate: { appName: appName }\"></span>\n </ion-card-title>\n <ion-card-subtitle [innerHTML]=\"description\"></ion-card-subtitle>\n </ion-card-header>\n <ion-card-content class=\"ion-no-padding\">\n <ion-text color=\"primary\">\n <img *ngIf=\"logo\" [attr.src]=\"logo\" alt=\"Logo\" />\n </ion-text>\n <!-- register help text -->\n <ion-text *ngIf=\"!isLogin && canRegister\">\n <br />\n <span translate>HOME.REGISTER_HELP</span>\n </ion-text>\n <div class=\"ion-padding-top\">\n <!-- If NOT login -->\n <ng-container *ngIf=\"!isLogin; else loginButtons\">\n <ion-button *ngIf=\"canRegister\" expand=\"block\" color=\"tertiary\" (click)=\"register()\">\n <span translate>HOME.BTN_REGISTER</span>\n </ion-button>\n <ion-button expand=\"block\" color=\"light\" [routerLink]=\"['/']\" (click)=\"login()\">\n <span translate>AUTH.BTN_LOGIN</span>\n </ion-button>\n </ng-container>\n\n <!-- If user login -->\n <ng-template #loginButtons>\n <!-- Feature buttons -->\n <ng-container *rxIf=\"$filteredButtons; let buttons\">\n <ng-container *ngFor=\"let item of buttons\">\n <ion-button\n *ngIf=\"item.path\"\n expand=\"block\"\n color=\"tertiary\"\n [class]=\"item.cssClass\"\n [routerLink]=\"item.path\"\n routerDirection=\"root\"\n >\n <ion-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.icon\" [name]=\"item.icon\"></ion-icon>\n <mat-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.matIcon\">{{ item.matIcon }}</mat-icon>\n <ion-text>{{ 'HOME.BTN_DATA_ENTRY' | translate: { name: (item.title | translate) } }}</ion-text>\n </ion-button>\n\n <!-- divider -->\n <div *ngIf=\"!item.path && !item.action\" [class]=\"item.cssClass\">\n <ion-label translate>{{ item.title }}&nbsp;</ion-label>\n </div>\n </ng-container>\n\n <!--<p *ngIf=\"buttons.length\" class=\"visible-mobile\">&nbsp;</p>-->\n </ng-container>\n\n <ion-button *ngIf=\"showAccountButton\" expand=\"block\" color=\"secondary\" [routerLink]=\"['/account']\">\n <ion-icon slot=\"start\" class=\"ion-float-start\" name=\"person-circle\"></ion-icon>\n <ion-text translate>HOME.BTN_MY_ACCOUNT</ion-text>\n </ion-button>\n\n <p hidden-xs hidden-sm hidden-mobile>\n <ion-text\n [innerHTML]=\"'HOME.NOT_THIS_ACCOUNT_QUESTION' | translate: { displayName: accountName }\"\n ></ion-text>\n <br />\n <ion-text>\n <a href=\"#\" (click)=\"logout($event)\">\n <span translate>HOME.BTN_DISCONNECT</span>\n </a>\n </ion-text>\n </p>\n </ng-template>\n </div>\n </ion-card-content>\n </ion-card>\n\n <!-- Page history -->\n @let pageHistory = isLogin && (pageHistory$ | push);\n @if (pageHistory | isNotEmptyArray) {\n <ion-grid class=\"history-container ion-align-self-center\">\n <ion-row>\n @for (page of pageHistory; track page.path) {\n <ion-col size=\"12\" size-xl=\"\" class=\"ion-text-center\">\n <ion-card class=\"ion-align-self-start ion-text-start\" @fadeInAnimation>\n <ion-card-header class=\"ion-no-padding\">\n <!-- top bar -->\n <ion-card-subtitle>\n <button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"removePageHistory(page.path)\"\n mat-icon-button\n class=\"ion-float-start ion-no-margin close-button\"\n >\n <mat-icon>close</mat-icon>\n </button>\n <ion-label [innerHTML]=\"page.subtitle | translate\"></ion-label>\n <ion-text class=\"ion-float-end\" [title]=\"page.time | dateFormat: { time: true }\">\n <small>\n <ion-icon name=\"time-outline\"></ion-icon>\n &nbsp;{{ page.time | dateFromNow }}\n </small>\n &nbsp;\n </ion-text>\n </ion-card-subtitle>\n\n <!-- main page -->\n <ion-card-title class=\"ion-no-margin ion-no-padding\">\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"page.path\"\n routerDirection=\"root\"\n lines=\"none\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"page.icon\" slot=\"start\" [name]=\"page.icon\"></ion-icon>\n <mat-icon *ngIf=\"page.matIcon\" slot=\"start\">{{ page.matIcon }}</mat-icon>\n\n <ion-label color=\"primary\" [innerHTML]=\"page.title\"></ion-label>\n </ion-item>\n </ion-card-title>\n </ion-card-header>\n\n <!-- children pages -->\n @if (page.children | isNotEmptyArray) {\n <ion-card-content class=\"ion-no-padding ion-padding-start\">\n <ion-list class=\"ion-no-padding\">\n @for (childPage of page.children; track childPage.path) {\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"childPage.path\"\n routerDirection=\"root\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"childPage.icon\" slot=\"start\" color=\"dark\" [name]=\"childPage.icon\"></ion-icon>\n <mat-icon *ngIf=\"childPage.matIcon\" slot=\"start\" color=\"dark\">\n {{ childPage.matIcon }}\n </mat-icon>\n\n <ion-label color=\"dark\" [innerHTML]=\"childPage.title\"></ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-card-content>\n }\n </ion-card>\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n } @else if (showPartnerBanner || showLegalInformation) {\n <ng-container *ngTemplateOutlet=\"bottomBanner\"></ng-container>\n }\n }\n</ion-content>\n\n<ng-template #bottomBanner>\n <ion-grid class=\"bottom-banner ion-text-center\" [class.floating]=\"!mobile\" @fadeInAnimation>\n <ion-row>\n @if (showPartnerBanner) {\n <ion-col size=\"12\" [sizeLg]=\"partnerBannerSize\" class=\"ion-text-center\">\n @if (showLegalInformation) {\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.PARTNERS' | translate }}</ion-list-header>\n </ion-list>\n }\n <!-- partners -->\n @for (item of $partners | async; track $index) {\n <a href=\"{{ item.siteUrl }}\">\n <img class=\"logo\" src=\"{{ item.logo }}\" alt=\"{{ item.label }}\" [title]=\"item.label\" />\n </a>\n }\n </ion-col>\n }\n @if (showLegalInformation) {\n <ion-col size=\"12\" size-sm=\"6\" size-lg=\"3\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header class=\"ion-text-left\">{{ 'HOME.LEGAL_INFORMATION' | translate }}</ion-list-header>\n @for (item of legalInformation; track item.path) {\n <ion-item (click)=\"openMarkdownModal($event, item)\" lines=\"none\" [button]=\"true\">\n <ion-label>\n <p>\n {{ item.title | translate: item.titleArgs }}\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-col>\n @if (showInstallLinks) {\n <ion-col size=\"12\" size-lg=\"3\" offset-lg=\"9\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.INSTALL_APP' | translate }}</ion-list-header>\n <!-- TODO -->\n </ion-list>\n </ion-col>\n }\n }\n </ion-row>\n </ion-grid>\n</ng-template>\n", styles: [":host{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .7)}h1,h2,h3,h4,h5{white-space:normal}.center,ion-content ion-card.welcome img{text-align:center;display:inline-block}ion-content{--background: transparent;background-size:cover;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-color:var(--ion-background-color);display:inline-block;padding:15px}ion-content .loading-page{display:block;height:100%;width:100%;position:absolute;background-color:transparent;z-index:5;transition:background .5s linear;-webkit-transition:background 1.5s linear}ion-content .loading-page.hidden{background-color:rgba(var(--ion-color-primary),0)}ion-content ion-card{z-index:9}ion-content ion-card ion-card-header{display:block}ion-content ion-card ion-card-header ion-card-subtitle,ion-content ion-card ion-card-header ion-card-title{display:block;width:100%}ion-content ion-card.main{min-width:240px;max-width:400px;margin-left:auto;margin-right:auto}ion-content ion-card.welcome{background-color:var(--ion-card-background-color)}ion-content ion-card.welcome button{margin-top:16px}ion-content ion-card.welcome img{max-width:250px}ion-content .history-container{margin-left:auto;margin-right:auto}ion-content .history-container ion-card{display:inline-block;box-sizing:border-box;z-index:8;min-width:240px;max-width:400px;width:100%;margin-left:auto;margin-right:auto;background-color:var(--ion-card-background-color)}ion-content .history-container ion-card ion-card-header ion-card-subtitle{height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle button[float-start]{--mdc-icon-button-state-layer-size: 40px;padding:8px;z-index:99;margin:0}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label{line-height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label[float-end]{padding-right:16px;font-weight:400}ion-content .history-container ion-card ion-card-header ion-card-title ion-item{width:100%;--ion-item-background: $transparent;--ion-item-icon-color: var(--ion-color-primary);--ion-item-text-color: var(--ion-color-primary)}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-header ion-card-title ion-item mat-icon[slot=start]{margin-right:unset;margin-inline-end:16px!important;color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-text,ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-label{color:var(--ion-item-text-color)!important;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}ion-content .history-container ion-card ion-card-content ion-list{--ion-item-background: var(--ion-color-transparent)}ion-content .history-container ion-card ion-card-content ion-list ion-item{--ion-item-icon-color: var(--ion-color-dark);--ion-item-text-color: var(--ion-color-dark)}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-content ion-list ion-item mat-icon[slot=start]{color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor;margin-right:unset;margin-inline-end:16px!important}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-text,ion-content .history-container ion-card ion-card-content ion-list ion-item ion-label{color:var(--ion-item-text-color)!important}ion-content .bottom-banner{background-color:var(--ion-card-background-color);min-width:240px;z-index:0}ion-content .bottom-banner.floating{position:absolute;left:16px;right:16px;bottom:16px}ion-content .bottom-banner img.logo{max-height:50px;margin-left:8px}ion-content ion-text{text-align:center}@media screen and (max-width: 575px){ion-content ion-card{min-width:160px;width:calc(100% - 20px);max-width:100%;margin-left:10px;margin-right:10px}ion-content ion-card h1{margin-top:0;font-size:20px}ion-content ion-card ion-card-content ion-button{margin-top:8px}ion-content .bottom-banner.floating{display:block;position:unset}ion-content .bottom-banner img.logo{max-height:30px}}@media screen and (max-width: 767px) and (min-width: 576px){ion-content{min-height:555px}ion-content .bottom-banner img.logo{max-height:40px}}@media screen and (min-width: 768px){ion-content{min-height:calc(100% - 100px)}}.content-credits{position:absolute;right:0;bottom:0}.content-credits ion-text{padding-inline-start:4px;padding-inline-end:4px;font-size:.8rem;background-color:var(--ion-background-color, white);color:var(--ion-text-color);opacity:.65}@media screen and (min-width: 992px){.left-border{border-left:1px solid var(--ion-color-medium)}}.bottom-banner ion-list-header{min-height:30px}.bottom-banner ion-item{--min-height: 28px}.bottom-banner ion-item ion-label{margin-top:3px!important;margin-bottom:3px!important}\n"], dependencies: [{ kind: "directive", type: i3$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i3$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i2$1.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$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$1.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$1.IonListHeader, selector: "ion-list-header", inputs: ["color", "lines", "mode"] }, { kind: "component", type: i2$1.IonMenuToggle, selector: "ion-menu-toggle", inputs: ["autoHide", "menu"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i2$1.RouterLinkDelegate, selector: ":not(a):not(area)[routerLink]" }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: i8$2.RxIf, selector: "[rxIf]", inputs: ["rxIf", "rxIfStrategy", "rxIfElse", "rxIfThen", "rxIfSuspense", "rxIfComplete", "rxIfError", "rxIfContextTrigger", "rxIfNextTrigger", "rxIfSuspenseTrigger", "rxIfErrorTrigger", "rxIfCompleteTrigger", "rxIfParent", "rxIfPatchZone", "rxIfRenderCallback"] }, { kind: "component", type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i8$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8$1.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i8$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["progressBarMode", "title", "color", "class", "id", "backHref", "defaultBackHref", "hasValidate", "hasClose", "hasSearch", "canGoBack", "canShowMenu"], outputs: ["onValidate", "onClose", "onValidateAndClose", "onBackClick", "onSearch"] }, { kind: "directive", type: i1$6.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: AppInstallUpgradeCard, selector: "app-install-upgrade-card", inputs: ["isLogin", "showUpgradeWarning", "showOfflineWarning", "showInstallButton", "debug"] }, { kind: "component", type: UserEventNotificationIcon, selector: "app-user-event-notification-icon", inputs: ["titleI18n", "disabled", "filter", "autoHide", "headerActions", "footerActions", "style", "color", "unreadColor", "fill", "mobile", "listStyle", "debug"], outputs: ["showList"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: i19.RxPush, name: "push" }, { kind: "pipe", type: DateFormatPipe, name: "dateFormat" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: IsNotNilOrBlankPipe, name: "isNotNilOrBlank" }], animations: [fadeInAnimation, slideUpDownAnimation], changeDetection: i0.ChangeDetectionStrategy.OnPush });
34139
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: HomePage, selector: "app-page-home", usesInheritance: true, ngImport: i0, template: "<app-toolbar [canGoBack]=\"false\" visible-xs visible-sm visible-mobile>\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon [autoHide]=\"autoHideNotificationIcon\"></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button (click)=\"toggleDarkMode()\">\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'sunny' : 'moon'\"></ion-icon>\n </ion-button>\n }\n\n <!-- Change locale button -->\n <ion-button [matMenuTriggerFor]=\"localeMenu\">\n <ion-icon slot=\"start\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n</app-toolbar>\n\n<!-- Change locale menu -->\n<mat-menu #localeMenu=\"matMenu\">\n <ng-template matMenuContent>\n @for (item of locales; track item.key) {\n <button mat-menu-item (click)=\"changeLanguage(item.key)\">\n <!--<mat-icon>{{ currentLocale === item.key ? 'checkmark' : undefined }}</mat-icon>-->\n <ion-label>{{ item.value }}</ion-label>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<ion-content [ngStyle]=\"contentStyle\" no-padding-xs>\n\n @if (contentCredits | isNotNilOrBlank) {\n <span class=\"content-credits\">\n <ion-text>{{ contentCredits }}</ion-text>\n </span>\n }\n\n <!-- loading spinner -->\n <div class=\"loading-page\" [class.cdk-visually-hidden]=\"!(loading$ | push)\">\n @if (showSpinner) {\n <div class=\"spinner\">\n <p [innerHTML]=\"'COMMON.LOADING_DOTS' | translate\"></p>\n <div class=\"sk-cube1 sk-cube\"></div>\n <div class=\"sk-cube2 sk-cube\"></div>\n <div class=\"sk-cube4 sk-cube\"></div>\n <div class=\"sk-cube3 sk-cube\"></div>\n </div>\n }\n </div>\n\n <!-- Desktop: translucent top toolbar -->\n @if (!mobile) {\n <div class=\"hidden-xs hidden-sm\">\n <ion-toolbar color=\"traansparent\" translucent>\n <ion-buttons slot=\"start\">\n <ion-menu-toggle>\n <ion-button color=\"light\" fill=\"clear\">\n <ion-icon slot=\"icon-only\" name=\"menu\"></ion-icon>\n </ion-button>\n </ion-menu-toggle>\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon\n [style]=\"'ion-button'\"\n color=\"secondary\"\n unreadColor=\"tertiary\"\n fill=\"solid\"\n [autoHide]=\"autoHideNotificationIcon\"\n ></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n (click)=\"toggleDarkMode()\"\n [title]=\"'HOME.BTN_DARK_MODE' | translate\"\n >\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'moon' : 'sunny'\"></ion-icon>\n </ion-button>\n }\n <!-- change locale button -->\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n [matMenuTriggerFor]=\"localeMenu\"\n [title]=\"'SETTINGS.LOCALE' | translate\"\n >\n <ion-icon slot=\"icon-only\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </div>\n }\n\n <!-- Install (and upgrade) card -->\n <app-install-upgrade-card [isLogin]=\"isLogin\" [showInstallButton]=\"true\"></app-install-upgrade-card>\n\n @if (!(loading$ | push)) {\n @let hasFeeds = feeds && (feeds.hasFeeds$ | async);\n <ion-grid class=\"ion-no-padding\">\n <ion-row>\n <ion-col size=\"0\" [sizeXl]=\"hasFeeds ? 3 : 0\"></ion-col>\n <ion-col>\n <!-- Welcome card -->\n <ion-card class=\"main welcome ion-padding ion-text-center ion-align-self-center\" @fadeInAnimation>\n <ion-card-header>\n <ion-card-title class=\"ion-text-center\">\n <span *ngIf=\"isWeb\" [innerHTML]=\"'HOME.WELCOME_WEB' | translate: { appName: appName }\"></span>\n <span *ngIf=\"!isWeb\" [innerHTML]=\"'HOME.WELCOME_APP' | translate: { appName: appName }\"></span>\n </ion-card-title>\n <ion-card-subtitle [innerHTML]=\"description\"></ion-card-subtitle>\n </ion-card-header>\n <ion-card-content class=\"ion-no-padding\">\n <ion-text color=\"primary\">\n <img *ngIf=\"logo\" [attr.src]=\"logo\" alt=\"Logo\" />\n </ion-text>\n <!-- register help text -->\n <ion-text *ngIf=\"!isLogin && canRegister\">\n <br />\n <span translate>HOME.REGISTER_HELP</span>\n </ion-text>\n <div class=\"ion-padding-top\">\n <!-- If NOT login -->\n <ng-container *ngIf=\"!isLogin; else loginButtons\">\n <ion-button *ngIf=\"canRegister\" expand=\"block\" color=\"tertiary\" (click)=\"register()\">\n <span translate>HOME.BTN_REGISTER</span>\n </ion-button>\n <ion-button expand=\"block\" color=\"light\" [routerLink]=\"['/']\" (click)=\"login()\">\n <span translate>AUTH.BTN_LOGIN</span>\n </ion-button>\n </ng-container>\n\n <!-- If user login -->\n <ng-template #loginButtons>\n <!-- Feature buttons -->\n <ng-container *rxIf=\"$filteredButtons; let buttons\">\n <ng-container *ngFor=\"let item of buttons\">\n <ion-button\n *ngIf=\"item.path\"\n expand=\"block\"\n color=\"tertiary\"\n [class]=\"item.cssClass\"\n [routerLink]=\"item.path\"\n routerDirection=\"root\"\n >\n <ion-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.icon\" [name]=\"item.icon\"></ion-icon>\n <mat-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.matIcon\">{{ item.matIcon }}</mat-icon>\n <ion-text>{{ 'HOME.BTN_DATA_ENTRY' | translate: { name: (item.title | translate) } }}</ion-text>\n </ion-button>\n\n <!-- divider -->\n <div *ngIf=\"!item.path && !item.action\" [class]=\"item.cssClass\">\n <ion-label translate>{{ item.title }}&nbsp;</ion-label>\n </div>\n </ng-container>\n\n <!--<p *ngIf=\"buttons.length\" class=\"visible-mobile\">&nbsp;</p>-->\n </ng-container>\n\n <ion-button *ngIf=\"showAccountButton\" expand=\"block\" color=\"secondary\" [routerLink]=\"['/account']\">\n <ion-icon slot=\"start\" class=\"ion-float-start\" name=\"person-circle\"></ion-icon>\n <ion-text translate>HOME.BTN_MY_ACCOUNT</ion-text>\n </ion-button>\n\n <p hidden-xs hidden-sm hidden-mobile>\n <ion-text\n [innerHTML]=\"'HOME.NOT_THIS_ACCOUNT_QUESTION' | translate: { displayName: accountName }\"\n ></ion-text>\n <br />\n <ion-text>\n <a href=\"#\" (click)=\"logout($event)\">\n <span translate>HOME.BTN_DISCONNECT</span>\n </a>\n </ion-text>\n </p>\n </ng-template>\n </div>\n </ion-card-content>\n </ion-card>\n </ion-col>\n <ion-col size=\"0\" [sizeXl]=\"hasFeeds ? 1 : 0\"></ion-col>\n <ion-col class=\"feed\" [size]=\"hasFeeds ? 12 : 0\" [sizeXl]=\"hasFeeds ? 4 : 0\">\n <app-feeds #feeds></app-feeds>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n\n\n <!-- Page history -->\n @let pageHistory = isLogin && (pageHistory$ | push);\n @if (pageHistory | isNotEmptyArray) {\n <ion-grid class=\"history-container ion-align-self-center\">\n <ion-row>\n @for (page of pageHistory; track page.path) {\n <ion-col size=\"12\" size-xl=\"\" class=\"ion-text-center\">\n <ion-card class=\"ion-align-self-start ion-text-start\" @fadeInAnimation>\n <ion-card-header class=\"ion-no-padding\">\n <!-- top bar -->\n <ion-card-subtitle>\n <button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"removePageHistory(page.path)\"\n mat-icon-button\n class=\"ion-float-start ion-no-margin close-button\"\n >\n <mat-icon>close</mat-icon>\n </button>\n <ion-label [innerHTML]=\"page.subtitle | translate\"></ion-label>\n <ion-text class=\"ion-float-end\" [title]=\"page.time | dateFormat: { time: true }\">\n <small>\n <ion-icon name=\"time-outline\"></ion-icon>\n &nbsp;{{ page.time | dateFromNow }}\n </small>\n &nbsp;\n </ion-text>\n </ion-card-subtitle>\n\n <!-- main page -->\n <ion-card-title class=\"ion-no-margin ion-no-padding\">\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"page.path\"\n routerDirection=\"root\"\n lines=\"none\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"page.icon\" slot=\"start\" [name]=\"page.icon\"></ion-icon>\n <mat-icon *ngIf=\"page.matIcon\" slot=\"start\">{{ page.matIcon }}</mat-icon>\n\n <ion-label color=\"primary\" [innerHTML]=\"page.title\"></ion-label>\n </ion-item>\n </ion-card-title>\n </ion-card-header>\n\n <!-- children pages -->\n @if (page.children | isNotEmptyArray) {\n <ion-card-content class=\"ion-no-padding ion-padding-start\">\n <ion-list class=\"ion-no-padding\">\n @for (childPage of page.children; track childPage.path) {\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"childPage.path\"\n routerDirection=\"root\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"childPage.icon\" slot=\"start\" color=\"dark\" [name]=\"childPage.icon\"></ion-icon>\n <mat-icon *ngIf=\"childPage.matIcon\" slot=\"start\" color=\"dark\">\n {{ childPage.matIcon }}\n </mat-icon>\n\n <ion-label color=\"dark\" [innerHTML]=\"childPage.title\"></ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-card-content>\n }\n </ion-card>\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n } @else if (showPartnerBanner || showLegalInformation) {\n <ng-container *ngTemplateOutlet=\"bottomBanner; context: { hasFeeds: hasFeeds }\"></ng-container>\n }\n\n }\n\n</ion-content>\n\n<ng-template #bottomBanner let-hasFeeds=\"hasFeeds\">\n <ion-grid class=\"bottom-banner ion-text-center\" [class.floating]=\"!mobile && !hasFeeds\" @fadeInAnimation>\n <ion-row>\n @if (showPartnerBanner) {\n <ion-col size=\"12\" [sizeLg]=\"partnerBannerSize\" class=\"ion-text-center\">\n @if (showLegalInformation) {\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.PARTNERS' | translate }}</ion-list-header>\n </ion-list>\n }\n <!-- partners -->\n @for (item of $partners | async; track $index) {\n <a href=\"{{ item.siteUrl }}\">\n <img class=\"logo\" src=\"{{ item.logo }}\" alt=\"{{ item.label }}\" [title]=\"item.label\" />\n </a>\n }\n </ion-col>\n }\n @if (showLegalInformation) {\n <ion-col size=\"12\" size-sm=\"6\" size-lg=\"3\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header class=\"ion-text-left\">{{ 'HOME.LEGAL_INFORMATION' | translate }}</ion-list-header>\n @for (item of legalInformation; track item.path) {\n <ion-item (click)=\"openMarkdownModal($event, item)\" lines=\"none\" [button]=\"true\">\n <ion-label>\n <p>\n {{ item.title | translate: item.titleArgs }}\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-col>\n @if (showInstallLinks) {\n <ion-col size=\"12\" size-lg=\"3\" offset-lg=\"9\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.INSTALL_APP' | translate }}</ion-list-header>\n <!-- TODO -->\n </ion-list>\n </ion-col>\n }\n }\n </ion-row>\n </ion-grid>\n</ng-template>\n", styles: [":host{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .7)}h1,h2,h3,h4,h5{white-space:normal}.center,ion-content ion-card.welcome img{text-align:center;display:inline-block}ion-content{--background: transparent;background-size:cover;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-color:var(--ion-background-color);display:inline-block;padding:15px}ion-content .loading-page{display:block;height:100%;width:100%;position:absolute;background-color:transparent;z-index:5;transition:background .5s linear;-webkit-transition:background 1.5s linear}ion-content .loading-page.hidden{background-color:rgba(var(--ion-color-primary),0)}ion-content ion-card{z-index:9;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content ion-card ion-card-header{display:block}ion-content ion-card ion-card-header ion-card-subtitle,ion-content ion-card ion-card-header ion-card-title{display:block;width:100%}ion-content ion-card.main{min-width:240px;max-width:400px;margin-left:auto;margin-right:auto}ion-content ion-card.welcome{background-color:var(--ion-card-background-color)}ion-content ion-card.welcome button{margin-top:16px}ion-content ion-card.welcome img{max-width:250px}ion-content .history-container{margin-left:auto;margin-right:auto}ion-content .history-container ion-card{display:inline-block;box-sizing:border-box;z-index:8;min-width:240px;max-width:400px;width:100%;margin-left:auto;margin-right:auto;background-color:var(--ion-card-background-color);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content .history-container ion-card ion-card-header ion-card-subtitle{height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle button[float-start]{--mdc-icon-button-state-layer-size: 40px;padding:8px;z-index:99;margin:0}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label{line-height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label[float-end]{padding-right:16px;font-weight:400}ion-content .history-container ion-card ion-card-header ion-card-title ion-item{width:100%;--ion-item-background: $transparent;--ion-item-icon-color: var(--ion-color-primary);--ion-item-text-color: var(--ion-color-primary)}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-header ion-card-title ion-item mat-icon[slot=start]{margin-right:unset;margin-inline-end:16px!important;color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-text,ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-label{color:var(--ion-item-text-color)!important;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}ion-content .history-container ion-card ion-card-content ion-list{--ion-item-background: var(--ion-color-transparent)}ion-content .history-container ion-card ion-card-content ion-list ion-item{--ion-item-icon-color: var(--ion-color-dark);--ion-item-text-color: var(--ion-color-dark)}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-content ion-list ion-item mat-icon[slot=start]{color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor;margin-right:unset;margin-inline-end:16px!important}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-text,ion-content .history-container ion-card ion-card-content ion-list ion-item ion-label{color:var(--ion-item-text-color)!important}ion-content .bottom-banner{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .5);background-color:var(--ion-card-background-color);min-width:240px;z-index:0;border-radius:4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin-left:16px;margin-right:16px;margin-bottom:16px}ion-content .bottom-banner img.logo{max-height:50px;margin-left:8px}ion-content .bottom-banner ion-list-header{min-height:30px}ion-content .bottom-banner ion-item{--min-height: 28px}ion-content .bottom-banner ion-item ion-label{margin-top:3px!important;margin-bottom:3px!important}ion-content ion-text{text-align:center}@media screen and (max-width: 575px){ion-content ion-card{min-width:160px;width:calc(100% - 20px);max-width:100%;margin-left:10px;margin-right:10px}ion-content ion-card h1{margin-top:0;font-size:20px}ion-content ion-card ion-card-content ion-button{margin-top:8px}ion-content .bottom-banner.floating{display:block;position:unset}ion-content .bottom-banner img.logo{max-height:30px}}@media screen and (max-width: 767px) and (min-width: 576px){ion-content{min-height:555px}ion-content .bottom-banner img.logo{max-height:40px}}@media screen and (min-width: 768px){ion-content{min-height:calc(100% - 100px)}}@media screen and (min-width: 1200px){ion-content ion-col.feed{min-height:calc(100vh - var(--ion-toolbar-height) - 138px)}ion-content .bottom-banner.floating{position:absolute;left:0;right:0;bottom:0}}.content-credits{position:absolute;right:0;bottom:0}.content-credits ion-text{padding-inline-start:4px;padding-inline-end:4px;font-size:.8rem;background-color:var(--ion-background-color, white);color:var(--ion-text-color);opacity:.65}@media screen and (min-width: 992px){.left-border{border-left:1px solid var(--ion-color-medium)}}\n"], dependencies: [{ kind: "directive", type: i3$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i3$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i3$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: i2$1.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$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$1.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$1.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCardTitle, selector: "ion-card-title", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "counter", "counterFormatter", "detail", "detailIcon", "disabled", "download", "fill", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "shape", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$1.IonListHeader, selector: "ion-list-header", inputs: ["color", "lines", "mode"] }, { kind: "component", type: i2$1.IonMenuToggle, selector: "ion-menu-toggle", inputs: ["autoHide", "menu"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i2$1.RouterLinkDelegate, selector: ":not(a):not(area)[routerLink]" }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "directive", type: i8$2.RxIf, selector: "[rxIf]", inputs: ["rxIf", "rxIfStrategy", "rxIfElse", "rxIfThen", "rxIfSuspense", "rxIfComplete", "rxIfError", "rxIfContextTrigger", "rxIfNextTrigger", "rxIfSuspenseTrigger", "rxIfErrorTrigger", "rxIfCompleteTrigger", "rxIfParent", "rxIfPatchZone", "rxIfRenderCallback"] }, { kind: "component", type: i6$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "component", type: i8$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i8$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i8$1.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i8$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "component", type: ToolbarComponent, selector: "app-toolbar", inputs: ["progressBarMode", "title", "color", "class", "id", "backHref", "defaultBackHref", "hasValidate", "hasClose", "hasSearch", "canGoBack", "canShowMenu"], outputs: ["onValidate", "onClose", "onValidateAndClose", "onBackClick", "onSearch"] }, { kind: "directive", type: i1$6.RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "component", type: AppInstallUpgradeCard, selector: "app-install-upgrade-card", inputs: ["isLogin", "showUpgradeWarning", "showOfflineWarning", "showInstallButton", "debug"] }, { kind: "component", type: UserEventNotificationIcon, selector: "app-user-event-notification-icon", inputs: ["titleI18n", "disabled", "filter", "autoHide", "headerActions", "footerActions", "style", "color", "unreadColor", "fill", "mobile", "listStyle", "debug"], outputs: ["showList"] }, { kind: "component", type: FeedsComponent, selector: "app-feeds", inputs: ["feeds", "showHeader", "headerTextColor"] }, { kind: "pipe", type: i3$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: i19.RxPush, name: "push" }, { kind: "pipe", type: DateFormatPipe, name: "dateFormat" }, { kind: "pipe", type: DateFromNowPipe, name: "dateFromNow" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: IsNotNilOrBlankPipe, name: "isNotNilOrBlank" }], animations: [fadeInAnimation, slideUpDownAnimation], changeDetection: i0.ChangeDetectionStrategy.OnPush });
33645
34140
  }
33646
34141
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: HomePage, decorators: [{
33647
34142
  type: Component,
33648
- args: [{ selector: 'app-page-home', changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInAnimation, slideUpDownAnimation], template: "<app-toolbar [canGoBack]=\"false\" visible-xs visible-sm visible-mobile>\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon [autoHide]=\"autoHideNotificationIcon\"></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button (click)=\"toggleDarkMode()\">\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'sunny' : 'moon'\"></ion-icon>\n </ion-button>\n }\n\n <!-- Change locale button -->\n <ion-button [matMenuTriggerFor]=\"localeMenu\">\n <ion-icon slot=\"start\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n</app-toolbar>\n\n<!-- Change locale menu -->\n<mat-menu #localeMenu=\"matMenu\">\n <ng-template matMenuContent>\n @for (item of locales; track item.key) {\n <button mat-menu-item (click)=\"changeLanguage(item.key)\">\n <ion-label>{{ item.value }}</ion-label>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<ion-content [ngStyle]=\"contentStyle\" no-padding-xs>\n @if (contentCredits | isNotNilOrBlank) {\n <span class=\"content-credits\">\n <ion-text>{{ contentCredits }}</ion-text>\n </span>\n }\n\n <!-- loading spinner -->\n <div class=\"loading-page\" [class.cdk-visually-hidden]=\"!(loading$ | push)\">\n @if (showSpinner) {\n <div class=\"spinner\">\n <p [innerHTML]=\"'COMMON.LOADING_DOTS' | translate\"></p>\n <div class=\"sk-cube1 sk-cube\"></div>\n <div class=\"sk-cube2 sk-cube\"></div>\n <div class=\"sk-cube4 sk-cube\"></div>\n <div class=\"sk-cube3 sk-cube\"></div>\n </div>\n }\n </div>\n\n <!-- Desktop: translucent top toolbar -->\n @if (!mobile) {\n <div class=\"hidden-xs hidden-sm\">\n <ion-toolbar color=\"traansparent\" translucent>\n <ion-buttons slot=\"start\">\n <ion-menu-toggle>\n <ion-button color=\"light\" fill=\"clear\">\n <ion-icon slot=\"icon-only\" name=\"menu\"></ion-icon>\n </ion-button>\n </ion-menu-toggle>\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon\n [style]=\"'ion-button'\"\n color=\"secondary\"\n unreadColor=\"tertiary\"\n fill=\"solid\"\n [autoHide]=\"autoHideNotificationIcon\"\n ></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n (click)=\"toggleDarkMode()\"\n [title]=\"'HOME.BTN_DARK_MODE' | translate\"\n >\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'moon' : 'sunny'\"></ion-icon>\n </ion-button>\n }\n <!-- change locale button -->\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n [matMenuTriggerFor]=\"localeMenu\"\n [title]=\"'SETTINGS.LOCALE' | translate\"\n >\n <ion-icon slot=\"icon-only\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </div>\n }\n\n <!-- Install (and upgrade) card -->\n <app-install-upgrade-card [isLogin]=\"isLogin\" [showInstallButton]=\"true\"></app-install-upgrade-card>\n\n @if (!(loading$ | push)) {\n <!-- Welcome card -->\n <ion-card class=\"main welcome ion-padding ion-text-center ion-align-self-center\" @fadeInAnimation>\n <ion-card-header>\n <ion-card-title class=\"ion-text-center\">\n <span *ngIf=\"isWeb\" [innerHTML]=\"'HOME.WELCOME_WEB' | translate: { appName: appName }\"></span>\n <span *ngIf=\"!isWeb\" [innerHTML]=\"'HOME.WELCOME_APP' | translate: { appName: appName }\"></span>\n </ion-card-title>\n <ion-card-subtitle [innerHTML]=\"description\"></ion-card-subtitle>\n </ion-card-header>\n <ion-card-content class=\"ion-no-padding\">\n <ion-text color=\"primary\">\n <img *ngIf=\"logo\" [attr.src]=\"logo\" alt=\"Logo\" />\n </ion-text>\n <!-- register help text -->\n <ion-text *ngIf=\"!isLogin && canRegister\">\n <br />\n <span translate>HOME.REGISTER_HELP</span>\n </ion-text>\n <div class=\"ion-padding-top\">\n <!-- If NOT login -->\n <ng-container *ngIf=\"!isLogin; else loginButtons\">\n <ion-button *ngIf=\"canRegister\" expand=\"block\" color=\"tertiary\" (click)=\"register()\">\n <span translate>HOME.BTN_REGISTER</span>\n </ion-button>\n <ion-button expand=\"block\" color=\"light\" [routerLink]=\"['/']\" (click)=\"login()\">\n <span translate>AUTH.BTN_LOGIN</span>\n </ion-button>\n </ng-container>\n\n <!-- If user login -->\n <ng-template #loginButtons>\n <!-- Feature buttons -->\n <ng-container *rxIf=\"$filteredButtons; let buttons\">\n <ng-container *ngFor=\"let item of buttons\">\n <ion-button\n *ngIf=\"item.path\"\n expand=\"block\"\n color=\"tertiary\"\n [class]=\"item.cssClass\"\n [routerLink]=\"item.path\"\n routerDirection=\"root\"\n >\n <ion-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.icon\" [name]=\"item.icon\"></ion-icon>\n <mat-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.matIcon\">{{ item.matIcon }}</mat-icon>\n <ion-text>{{ 'HOME.BTN_DATA_ENTRY' | translate: { name: (item.title | translate) } }}</ion-text>\n </ion-button>\n\n <!-- divider -->\n <div *ngIf=\"!item.path && !item.action\" [class]=\"item.cssClass\">\n <ion-label translate>{{ item.title }}&nbsp;</ion-label>\n </div>\n </ng-container>\n\n <!--<p *ngIf=\"buttons.length\" class=\"visible-mobile\">&nbsp;</p>-->\n </ng-container>\n\n <ion-button *ngIf=\"showAccountButton\" expand=\"block\" color=\"secondary\" [routerLink]=\"['/account']\">\n <ion-icon slot=\"start\" class=\"ion-float-start\" name=\"person-circle\"></ion-icon>\n <ion-text translate>HOME.BTN_MY_ACCOUNT</ion-text>\n </ion-button>\n\n <p hidden-xs hidden-sm hidden-mobile>\n <ion-text\n [innerHTML]=\"'HOME.NOT_THIS_ACCOUNT_QUESTION' | translate: { displayName: accountName }\"\n ></ion-text>\n <br />\n <ion-text>\n <a href=\"#\" (click)=\"logout($event)\">\n <span translate>HOME.BTN_DISCONNECT</span>\n </a>\n </ion-text>\n </p>\n </ng-template>\n </div>\n </ion-card-content>\n </ion-card>\n\n <!-- Page history -->\n @let pageHistory = isLogin && (pageHistory$ | push);\n @if (pageHistory | isNotEmptyArray) {\n <ion-grid class=\"history-container ion-align-self-center\">\n <ion-row>\n @for (page of pageHistory; track page.path) {\n <ion-col size=\"12\" size-xl=\"\" class=\"ion-text-center\">\n <ion-card class=\"ion-align-self-start ion-text-start\" @fadeInAnimation>\n <ion-card-header class=\"ion-no-padding\">\n <!-- top bar -->\n <ion-card-subtitle>\n <button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"removePageHistory(page.path)\"\n mat-icon-button\n class=\"ion-float-start ion-no-margin close-button\"\n >\n <mat-icon>close</mat-icon>\n </button>\n <ion-label [innerHTML]=\"page.subtitle | translate\"></ion-label>\n <ion-text class=\"ion-float-end\" [title]=\"page.time | dateFormat: { time: true }\">\n <small>\n <ion-icon name=\"time-outline\"></ion-icon>\n &nbsp;{{ page.time | dateFromNow }}\n </small>\n &nbsp;\n </ion-text>\n </ion-card-subtitle>\n\n <!-- main page -->\n <ion-card-title class=\"ion-no-margin ion-no-padding\">\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"page.path\"\n routerDirection=\"root\"\n lines=\"none\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"page.icon\" slot=\"start\" [name]=\"page.icon\"></ion-icon>\n <mat-icon *ngIf=\"page.matIcon\" slot=\"start\">{{ page.matIcon }}</mat-icon>\n\n <ion-label color=\"primary\" [innerHTML]=\"page.title\"></ion-label>\n </ion-item>\n </ion-card-title>\n </ion-card-header>\n\n <!-- children pages -->\n @if (page.children | isNotEmptyArray) {\n <ion-card-content class=\"ion-no-padding ion-padding-start\">\n <ion-list class=\"ion-no-padding\">\n @for (childPage of page.children; track childPage.path) {\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"childPage.path\"\n routerDirection=\"root\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"childPage.icon\" slot=\"start\" color=\"dark\" [name]=\"childPage.icon\"></ion-icon>\n <mat-icon *ngIf=\"childPage.matIcon\" slot=\"start\" color=\"dark\">\n {{ childPage.matIcon }}\n </mat-icon>\n\n <ion-label color=\"dark\" [innerHTML]=\"childPage.title\"></ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-card-content>\n }\n </ion-card>\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n } @else if (showPartnerBanner || showLegalInformation) {\n <ng-container *ngTemplateOutlet=\"bottomBanner\"></ng-container>\n }\n }\n</ion-content>\n\n<ng-template #bottomBanner>\n <ion-grid class=\"bottom-banner ion-text-center\" [class.floating]=\"!mobile\" @fadeInAnimation>\n <ion-row>\n @if (showPartnerBanner) {\n <ion-col size=\"12\" [sizeLg]=\"partnerBannerSize\" class=\"ion-text-center\">\n @if (showLegalInformation) {\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.PARTNERS' | translate }}</ion-list-header>\n </ion-list>\n }\n <!-- partners -->\n @for (item of $partners | async; track $index) {\n <a href=\"{{ item.siteUrl }}\">\n <img class=\"logo\" src=\"{{ item.logo }}\" alt=\"{{ item.label }}\" [title]=\"item.label\" />\n </a>\n }\n </ion-col>\n }\n @if (showLegalInformation) {\n <ion-col size=\"12\" size-sm=\"6\" size-lg=\"3\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header class=\"ion-text-left\">{{ 'HOME.LEGAL_INFORMATION' | translate }}</ion-list-header>\n @for (item of legalInformation; track item.path) {\n <ion-item (click)=\"openMarkdownModal($event, item)\" lines=\"none\" [button]=\"true\">\n <ion-label>\n <p>\n {{ item.title | translate: item.titleArgs }}\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-col>\n @if (showInstallLinks) {\n <ion-col size=\"12\" size-lg=\"3\" offset-lg=\"9\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.INSTALL_APP' | translate }}</ion-list-header>\n <!-- TODO -->\n </ion-list>\n </ion-col>\n }\n }\n </ion-row>\n </ion-grid>\n</ng-template>\n", styles: [":host{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .7)}h1,h2,h3,h4,h5{white-space:normal}.center,ion-content ion-card.welcome img{text-align:center;display:inline-block}ion-content{--background: transparent;background-size:cover;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-color:var(--ion-background-color);display:inline-block;padding:15px}ion-content .loading-page{display:block;height:100%;width:100%;position:absolute;background-color:transparent;z-index:5;transition:background .5s linear;-webkit-transition:background 1.5s linear}ion-content .loading-page.hidden{background-color:rgba(var(--ion-color-primary),0)}ion-content ion-card{z-index:9}ion-content ion-card ion-card-header{display:block}ion-content ion-card ion-card-header ion-card-subtitle,ion-content ion-card ion-card-header ion-card-title{display:block;width:100%}ion-content ion-card.main{min-width:240px;max-width:400px;margin-left:auto;margin-right:auto}ion-content ion-card.welcome{background-color:var(--ion-card-background-color)}ion-content ion-card.welcome button{margin-top:16px}ion-content ion-card.welcome img{max-width:250px}ion-content .history-container{margin-left:auto;margin-right:auto}ion-content .history-container ion-card{display:inline-block;box-sizing:border-box;z-index:8;min-width:240px;max-width:400px;width:100%;margin-left:auto;margin-right:auto;background-color:var(--ion-card-background-color)}ion-content .history-container ion-card ion-card-header ion-card-subtitle{height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle button[float-start]{--mdc-icon-button-state-layer-size: 40px;padding:8px;z-index:99;margin:0}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label{line-height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label[float-end]{padding-right:16px;font-weight:400}ion-content .history-container ion-card ion-card-header ion-card-title ion-item{width:100%;--ion-item-background: $transparent;--ion-item-icon-color: var(--ion-color-primary);--ion-item-text-color: var(--ion-color-primary)}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-header ion-card-title ion-item mat-icon[slot=start]{margin-right:unset;margin-inline-end:16px!important;color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-text,ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-label{color:var(--ion-item-text-color)!important;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}ion-content .history-container ion-card ion-card-content ion-list{--ion-item-background: var(--ion-color-transparent)}ion-content .history-container ion-card ion-card-content ion-list ion-item{--ion-item-icon-color: var(--ion-color-dark);--ion-item-text-color: var(--ion-color-dark)}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-content ion-list ion-item mat-icon[slot=start]{color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor;margin-right:unset;margin-inline-end:16px!important}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-text,ion-content .history-container ion-card ion-card-content ion-list ion-item ion-label{color:var(--ion-item-text-color)!important}ion-content .bottom-banner{background-color:var(--ion-card-background-color);min-width:240px;z-index:0}ion-content .bottom-banner.floating{position:absolute;left:16px;right:16px;bottom:16px}ion-content .bottom-banner img.logo{max-height:50px;margin-left:8px}ion-content ion-text{text-align:center}@media screen and (max-width: 575px){ion-content ion-card{min-width:160px;width:calc(100% - 20px);max-width:100%;margin-left:10px;margin-right:10px}ion-content ion-card h1{margin-top:0;font-size:20px}ion-content ion-card ion-card-content ion-button{margin-top:8px}ion-content .bottom-banner.floating{display:block;position:unset}ion-content .bottom-banner img.logo{max-height:30px}}@media screen and (max-width: 767px) and (min-width: 576px){ion-content{min-height:555px}ion-content .bottom-banner img.logo{max-height:40px}}@media screen and (min-width: 768px){ion-content{min-height:calc(100% - 100px)}}.content-credits{position:absolute;right:0;bottom:0}.content-credits ion-text{padding-inline-start:4px;padding-inline-end:4px;font-size:.8rem;background-color:var(--ion-background-color, white);color:var(--ion-text-color);opacity:.65}@media screen and (min-width: 992px){.left-border{border-left:1px solid var(--ion-color-medium)}}.bottom-banner ion-list-header{min-height:30px}.bottom-banner ion-item{--min-height: 28px}.bottom-banner ion-item ion-label{margin-top:3px!important;margin-bottom:3px!important}\n"] }]
34143
+ args: [{ selector: 'app-page-home', changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInAnimation, slideUpDownAnimation], template: "<app-toolbar [canGoBack]=\"false\" visible-xs visible-sm visible-mobile>\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon [autoHide]=\"autoHideNotificationIcon\"></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button (click)=\"toggleDarkMode()\">\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'sunny' : 'moon'\"></ion-icon>\n </ion-button>\n }\n\n <!-- Change locale button -->\n <ion-button [matMenuTriggerFor]=\"localeMenu\">\n <ion-icon slot=\"start\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n</app-toolbar>\n\n<!-- Change locale menu -->\n<mat-menu #localeMenu=\"matMenu\">\n <ng-template matMenuContent>\n @for (item of locales; track item.key) {\n <button mat-menu-item (click)=\"changeLanguage(item.key)\">\n <!--<mat-icon>{{ currentLocale === item.key ? 'checkmark' : undefined }}</mat-icon>-->\n <ion-label>{{ item.value }}</ion-label>\n </button>\n }\n </ng-template>\n</mat-menu>\n\n<ion-content [ngStyle]=\"contentStyle\" no-padding-xs>\n\n @if (contentCredits | isNotNilOrBlank) {\n <span class=\"content-credits\">\n <ion-text>{{ contentCredits }}</ion-text>\n </span>\n }\n\n <!-- loading spinner -->\n <div class=\"loading-page\" [class.cdk-visually-hidden]=\"!(loading$ | push)\">\n @if (showSpinner) {\n <div class=\"spinner\">\n <p [innerHTML]=\"'COMMON.LOADING_DOTS' | translate\"></p>\n <div class=\"sk-cube1 sk-cube\"></div>\n <div class=\"sk-cube2 sk-cube\"></div>\n <div class=\"sk-cube4 sk-cube\"></div>\n <div class=\"sk-cube3 sk-cube\"></div>\n </div>\n }\n </div>\n\n <!-- Desktop: translucent top toolbar -->\n @if (!mobile) {\n <div class=\"hidden-xs hidden-sm\">\n <ion-toolbar color=\"traansparent\" translucent>\n <ion-buttons slot=\"start\">\n <ion-menu-toggle>\n <ion-button color=\"light\" fill=\"clear\">\n <ion-icon slot=\"icon-only\" name=\"menu\"></ion-icon>\n </ion-button>\n </ion-menu-toggle>\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n @if (showNotificationIcon) {\n <app-user-event-notification-icon\n [style]=\"'ion-button'\"\n color=\"secondary\"\n unreadColor=\"tertiary\"\n fill=\"solid\"\n [autoHide]=\"autoHideNotificationIcon\"\n ></app-user-event-notification-icon>\n }\n\n <!-- dark mode (if allow by the environment token) -->\n @if (settings.allowDarkMode) {\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n (click)=\"toggleDarkMode()\"\n [title]=\"'HOME.BTN_DARK_MODE' | translate\"\n >\n <ion-icon slot=\"icon-only\" [name]=\"(darkMode$ | push) ? 'moon' : 'sunny'\"></ion-icon>\n </ion-button>\n }\n <!-- change locale button -->\n <ion-button\n color=\"secondary\"\n fill=\"solid\"\n [matMenuTriggerFor]=\"localeMenu\"\n [title]=\"'SETTINGS.LOCALE' | translate\"\n >\n <ion-icon slot=\"icon-only\" name=\"language\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n </div>\n }\n\n <!-- Install (and upgrade) card -->\n <app-install-upgrade-card [isLogin]=\"isLogin\" [showInstallButton]=\"true\"></app-install-upgrade-card>\n\n @if (!(loading$ | push)) {\n @let hasFeeds = feeds && (feeds.hasFeeds$ | async);\n <ion-grid class=\"ion-no-padding\">\n <ion-row>\n <ion-col size=\"0\" [sizeXl]=\"hasFeeds ? 3 : 0\"></ion-col>\n <ion-col>\n <!-- Welcome card -->\n <ion-card class=\"main welcome ion-padding ion-text-center ion-align-self-center\" @fadeInAnimation>\n <ion-card-header>\n <ion-card-title class=\"ion-text-center\">\n <span *ngIf=\"isWeb\" [innerHTML]=\"'HOME.WELCOME_WEB' | translate: { appName: appName }\"></span>\n <span *ngIf=\"!isWeb\" [innerHTML]=\"'HOME.WELCOME_APP' | translate: { appName: appName }\"></span>\n </ion-card-title>\n <ion-card-subtitle [innerHTML]=\"description\"></ion-card-subtitle>\n </ion-card-header>\n <ion-card-content class=\"ion-no-padding\">\n <ion-text color=\"primary\">\n <img *ngIf=\"logo\" [attr.src]=\"logo\" alt=\"Logo\" />\n </ion-text>\n <!-- register help text -->\n <ion-text *ngIf=\"!isLogin && canRegister\">\n <br />\n <span translate>HOME.REGISTER_HELP</span>\n </ion-text>\n <div class=\"ion-padding-top\">\n <!-- If NOT login -->\n <ng-container *ngIf=\"!isLogin; else loginButtons\">\n <ion-button *ngIf=\"canRegister\" expand=\"block\" color=\"tertiary\" (click)=\"register()\">\n <span translate>HOME.BTN_REGISTER</span>\n </ion-button>\n <ion-button expand=\"block\" color=\"light\" [routerLink]=\"['/']\" (click)=\"login()\">\n <span translate>AUTH.BTN_LOGIN</span>\n </ion-button>\n </ng-container>\n\n <!-- If user login -->\n <ng-template #loginButtons>\n <!-- Feature buttons -->\n <ng-container *rxIf=\"$filteredButtons; let buttons\">\n <ng-container *ngFor=\"let item of buttons\">\n <ion-button\n *ngIf=\"item.path\"\n expand=\"block\"\n color=\"tertiary\"\n [class]=\"item.cssClass\"\n [routerLink]=\"item.path\"\n routerDirection=\"root\"\n >\n <ion-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.icon\" [name]=\"item.icon\"></ion-icon>\n <mat-icon slot=\"start\" class=\"ion-float-start\" *ngIf=\"item.matIcon\">{{ item.matIcon }}</mat-icon>\n <ion-text>{{ 'HOME.BTN_DATA_ENTRY' | translate: { name: (item.title | translate) } }}</ion-text>\n </ion-button>\n\n <!-- divider -->\n <div *ngIf=\"!item.path && !item.action\" [class]=\"item.cssClass\">\n <ion-label translate>{{ item.title }}&nbsp;</ion-label>\n </div>\n </ng-container>\n\n <!--<p *ngIf=\"buttons.length\" class=\"visible-mobile\">&nbsp;</p>-->\n </ng-container>\n\n <ion-button *ngIf=\"showAccountButton\" expand=\"block\" color=\"secondary\" [routerLink]=\"['/account']\">\n <ion-icon slot=\"start\" class=\"ion-float-start\" name=\"person-circle\"></ion-icon>\n <ion-text translate>HOME.BTN_MY_ACCOUNT</ion-text>\n </ion-button>\n\n <p hidden-xs hidden-sm hidden-mobile>\n <ion-text\n [innerHTML]=\"'HOME.NOT_THIS_ACCOUNT_QUESTION' | translate: { displayName: accountName }\"\n ></ion-text>\n <br />\n <ion-text>\n <a href=\"#\" (click)=\"logout($event)\">\n <span translate>HOME.BTN_DISCONNECT</span>\n </a>\n </ion-text>\n </p>\n </ng-template>\n </div>\n </ion-card-content>\n </ion-card>\n </ion-col>\n <ion-col size=\"0\" [sizeXl]=\"hasFeeds ? 1 : 0\"></ion-col>\n <ion-col class=\"feed\" [size]=\"hasFeeds ? 12 : 0\" [sizeXl]=\"hasFeeds ? 4 : 0\">\n <app-feeds #feeds></app-feeds>\n </ion-col>\n </ion-row>\n </ion-grid>\n\n\n\n <!-- Page history -->\n @let pageHistory = isLogin && (pageHistory$ | push);\n @if (pageHistory | isNotEmptyArray) {\n <ion-grid class=\"history-container ion-align-self-center\">\n <ion-row>\n @for (page of pageHistory; track page.path) {\n <ion-col size=\"12\" size-xl=\"\" class=\"ion-text-center\">\n <ion-card class=\"ion-align-self-start ion-text-start\" @fadeInAnimation>\n <ion-card-header class=\"ion-no-padding\">\n <!-- top bar -->\n <ion-card-subtitle>\n <button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"removePageHistory(page.path)\"\n mat-icon-button\n class=\"ion-float-start ion-no-margin close-button\"\n >\n <mat-icon>close</mat-icon>\n </button>\n <ion-label [innerHTML]=\"page.subtitle | translate\"></ion-label>\n <ion-text class=\"ion-float-end\" [title]=\"page.time | dateFormat: { time: true }\">\n <small>\n <ion-icon name=\"time-outline\"></ion-icon>\n &nbsp;{{ page.time | dateFromNow }}\n </small>\n &nbsp;\n </ion-text>\n </ion-card-subtitle>\n\n <!-- main page -->\n <ion-card-title class=\"ion-no-margin ion-no-padding\">\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"page.path\"\n routerDirection=\"root\"\n lines=\"none\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"page.icon\" slot=\"start\" [name]=\"page.icon\"></ion-icon>\n <mat-icon *ngIf=\"page.matIcon\" slot=\"start\">{{ page.matIcon }}</mat-icon>\n\n <ion-label color=\"primary\" [innerHTML]=\"page.title\"></ion-label>\n </ion-item>\n </ion-card-title>\n </ion-card-header>\n\n <!-- children pages -->\n @if (page.children | isNotEmptyArray) {\n <ion-card-content class=\"ion-no-padding ion-padding-start\">\n <ion-list class=\"ion-no-padding\">\n @for (childPage of page.children; track childPage.path) {\n <ion-item\n detail=\"true\"\n tappable\n class=\"text-1x\"\n [routerLink]=\"childPage.path\"\n routerDirection=\"root\"\n >\n <!-- page icon-->\n <ion-icon *ngIf=\"childPage.icon\" slot=\"start\" color=\"dark\" [name]=\"childPage.icon\"></ion-icon>\n <mat-icon *ngIf=\"childPage.matIcon\" slot=\"start\" color=\"dark\">\n {{ childPage.matIcon }}\n </mat-icon>\n\n <ion-label color=\"dark\" [innerHTML]=\"childPage.title\"></ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-card-content>\n }\n </ion-card>\n </ion-col>\n }\n </ion-row>\n </ion-grid>\n } @else if (showPartnerBanner || showLegalInformation) {\n <ng-container *ngTemplateOutlet=\"bottomBanner; context: { hasFeeds: hasFeeds }\"></ng-container>\n }\n\n }\n\n</ion-content>\n\n<ng-template #bottomBanner let-hasFeeds=\"hasFeeds\">\n <ion-grid class=\"bottom-banner ion-text-center\" [class.floating]=\"!mobile && !hasFeeds\" @fadeInAnimation>\n <ion-row>\n @if (showPartnerBanner) {\n <ion-col size=\"12\" [sizeLg]=\"partnerBannerSize\" class=\"ion-text-center\">\n @if (showLegalInformation) {\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.PARTNERS' | translate }}</ion-list-header>\n </ion-list>\n }\n <!-- partners -->\n @for (item of $partners | async; track $index) {\n <a href=\"{{ item.siteUrl }}\">\n <img class=\"logo\" src=\"{{ item.logo }}\" alt=\"{{ item.label }}\" [title]=\"item.label\" />\n </a>\n }\n </ion-col>\n }\n @if (showLegalInformation) {\n <ion-col size=\"12\" size-sm=\"6\" size-lg=\"3\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header class=\"ion-text-left\">{{ 'HOME.LEGAL_INFORMATION' | translate }}</ion-list-header>\n @for (item of legalInformation; track item.path) {\n <ion-item (click)=\"openMarkdownModal($event, item)\" lines=\"none\" [button]=\"true\">\n <ion-label>\n <p>\n {{ item.title | translate: item.titleArgs }}\n </p>\n </ion-label>\n </ion-item>\n }\n </ion-list>\n </ion-col>\n @if (showInstallLinks) {\n <ion-col size=\"12\" size-lg=\"3\" offset-lg=\"9\" [class.left-border]=\"showPartnerBanner\">\n <ion-list class=\"legal-info\">\n <ion-list-header>{{ 'HOME.INSTALL_APP' | translate }}</ion-list-header>\n <!-- TODO -->\n </ion-list>\n </ion-col>\n }\n }\n </ion-row>\n </ion-grid>\n</ng-template>\n", styles: [":host{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .7)}h1,h2,h3,h4,h5{white-space:normal}.center,ion-content ion-card.welcome img{text-align:center;display:inline-block}ion-content{--background: transparent;background-size:cover;-webkit-background-size:cover;-moz-background-size:cover;-o-background-size:cover;background-color:var(--ion-background-color);display:inline-block;padding:15px}ion-content .loading-page{display:block;height:100%;width:100%;position:absolute;background-color:transparent;z-index:5;transition:background .5s linear;-webkit-transition:background 1.5s linear}ion-content .loading-page.hidden{background-color:rgba(var(--ion-color-primary),0)}ion-content ion-card{z-index:9;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content ion-card ion-card-header{display:block}ion-content ion-card ion-card-header ion-card-subtitle,ion-content ion-card ion-card-header ion-card-title{display:block;width:100%}ion-content ion-card.main{min-width:240px;max-width:400px;margin-left:auto;margin-right:auto}ion-content ion-card.welcome{background-color:var(--ion-card-background-color)}ion-content ion-card.welcome button{margin-top:16px}ion-content ion-card.welcome img{max-width:250px}ion-content .history-container{margin-left:auto;margin-right:auto}ion-content .history-container ion-card{display:inline-block;box-sizing:border-box;z-index:8;min-width:240px;max-width:400px;width:100%;margin-left:auto;margin-right:auto;background-color:var(--ion-card-background-color);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px)}ion-content .history-container ion-card ion-card-header ion-card-subtitle{height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle button[float-start]{--mdc-icon-button-state-layer-size: 40px;padding:8px;z-index:99;margin:0}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label{line-height:40px}ion-content .history-container ion-card ion-card-header ion-card-subtitle ion-label[float-end]{padding-right:16px;font-weight:400}ion-content .history-container ion-card ion-card-header ion-card-title ion-item{width:100%;--ion-item-background: $transparent;--ion-item-icon-color: var(--ion-color-primary);--ion-item-text-color: var(--ion-color-primary)}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-header ion-card-title ion-item mat-icon[slot=start]{margin-right:unset;margin-inline-end:16px!important;color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor}ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-text,ion-content .history-container ion-card ion-card-header ion-card-title ion-item ion-label{color:var(--ion-item-text-color)!important;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;-ms-text-size-adjust:100%;text-size-adjust:100%}ion-content .history-container ion-card ion-card-content ion-list{--ion-item-background: var(--ion-color-transparent)}ion-content .history-container ion-card ion-card-content ion-list ion-item{--ion-item-icon-color: var(--ion-color-dark);--ion-item-text-color: var(--ion-color-dark)}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-icon[slot=start],ion-content .history-container ion-card ion-card-content ion-list ion-item mat-icon[slot=start]{color:var(--ion-item-icon-color)!important;fill:currentColor;stroke:currentColor;margin-right:unset;margin-inline-end:16px!important}ion-content .history-container ion-card ion-card-content ion-list ion-item ion-text,ion-content .history-container ion-card ion-card-content ion-list ion-item ion-label{color:var(--ion-item-text-color)!important}ion-content .bottom-banner{--ion-card-background-color: rgba(var(--ion-background-color-rgb), .5);background-color:var(--ion-card-background-color);min-width:240px;z-index:0;border-radius:4px;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);margin-left:16px;margin-right:16px;margin-bottom:16px}ion-content .bottom-banner img.logo{max-height:50px;margin-left:8px}ion-content .bottom-banner ion-list-header{min-height:30px}ion-content .bottom-banner ion-item{--min-height: 28px}ion-content .bottom-banner ion-item ion-label{margin-top:3px!important;margin-bottom:3px!important}ion-content ion-text{text-align:center}@media screen and (max-width: 575px){ion-content ion-card{min-width:160px;width:calc(100% - 20px);max-width:100%;margin-left:10px;margin-right:10px}ion-content ion-card h1{margin-top:0;font-size:20px}ion-content ion-card ion-card-content ion-button{margin-top:8px}ion-content .bottom-banner.floating{display:block;position:unset}ion-content .bottom-banner img.logo{max-height:30px}}@media screen and (max-width: 767px) and (min-width: 576px){ion-content{min-height:555px}ion-content .bottom-banner img.logo{max-height:40px}}@media screen and (min-width: 768px){ion-content{min-height:calc(100% - 100px)}}@media screen and (min-width: 1200px){ion-content ion-col.feed{min-height:calc(100vh - var(--ion-toolbar-height) - 138px)}ion-content .bottom-banner.floating{position:absolute;left:0;right:0;bottom:0}}.content-credits{position:absolute;right:0;bottom:0}.content-credits ion-text{padding-inline-start:4px;padding-inline-end:4px;font-size:.8rem;background-color:var(--ion-background-color, white);color:var(--ion-text-color);opacity:.65}@media screen and (min-width: 992px){.left-border{border-left:1px solid var(--ion-color-medium)}}\n"] }]
33649
34144
  }], ctorParameters: () => [{ type: AccountService }, { type: i2$1.ModalController }, { type: i1$1.TranslateService }, { type: i2$1.ToastController }, { type: ConfigService }, { type: PlatformService }, { type: i0.ChangeDetectorRef }, { type: NetworkService }, { type: LocalSettingsService }, { type: Environment, decorators: [{
33650
34145
  type: Inject,
33651
34146
  args: [ENVIRONMENT]
@@ -33829,6 +34324,20 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
33829
34324
  }]
33830
34325
  }] });
33831
34326
 
34327
+ class FeedModule {
34328
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
34329
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: FeedModule, declarations: [FeedsComponent, FeedDirective], imports: [SharedModule, i1$1.TranslateModule, NgOptimizedImage], exports: [TranslateModule, FeedsComponent, FeedDirective] });
34330
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedModule, imports: [SharedModule, TranslateModule.forChild(), TranslateModule] });
34331
+ }
34332
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: FeedModule, decorators: [{
34333
+ type: NgModule,
34334
+ args: [{
34335
+ imports: [SharedModule, TranslateModule.forChild(), NgOptimizedImage],
34336
+ declarations: [FeedsComponent, FeedDirective],
34337
+ exports: [TranslateModule, FeedsComponent, FeedDirective],
34338
+ }]
34339
+ }] });
34340
+
33832
34341
  class AppHomePageModule {
33833
34342
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppHomePageModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
33834
34343
  static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.13", ngImport: i0, type: AppHomePageModule, declarations: [HomePage], imports: [SharedModule,
@@ -33837,7 +34346,8 @@ class AppHomePageModule {
33837
34346
  AppAuthModule,
33838
34347
  AppInstallUpgradeCardModule,
33839
34348
  SharedMarkdownModule,
33840
- UserEventModule], exports: [RouterModule, TranslateModule, HomePage] });
34349
+ UserEventModule,
34350
+ FeedModule], exports: [RouterModule, TranslateModule, HomePage] });
33841
34351
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppHomePageModule, imports: [SharedModule,
33842
34352
  RouterModule,
33843
34353
  TranslateModule.forChild(),
@@ -33846,7 +34356,8 @@ class AppHomePageModule {
33846
34356
  AppAuthModule,
33847
34357
  AppInstallUpgradeCardModule,
33848
34358
  SharedMarkdownModule,
33849
- UserEventModule, RouterModule, TranslateModule] });
34359
+ UserEventModule,
34360
+ FeedModule, RouterModule, TranslateModule] });
33850
34361
  }
33851
34362
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: AppHomePageModule, decorators: [{
33852
34363
  type: NgModule,
@@ -33861,6 +34372,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
33861
34372
  AppInstallUpgradeCardModule,
33862
34373
  SharedMarkdownModule,
33863
34374
  UserEventModule,
34375
+ FeedModule,
33864
34376
  ],
33865
34377
  declarations: [HomePage],
33866
34378
  exports: [RouterModule, TranslateModule, HomePage],
@@ -44296,6 +44808,167 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
44296
44808
  args: [ENVIRONMENT]
44297
44809
  }] }] });
44298
44810
 
44811
+ class DiscourseFeedService extends StartableService {
44812
+ settings;
44813
+ environment;
44814
+ discourseApis = new Map();
44815
+ _logPrefix = '[discourse-feed-service] ';
44816
+ _state = new RxState();
44817
+ network = inject(NetworkService);
44818
+ locale$;
44819
+ feedUrls$;
44820
+ locale;
44821
+ feedUrls;
44822
+ constructor(settings, environment) {
44823
+ super(settings);
44824
+ this.settings = settings;
44825
+ this.environment = environment;
44826
+ this._debug = !environment.production;
44827
+ if (this._debug)
44828
+ console.debug('[discourse-feed-service] Creating service');
44829
+ this._state.connect('locale', this.settings.locale$);
44830
+ this._state.connect('feedUrls', this.locale$.pipe(filter(isNotNilOrBlank), map((locale) => (locale && environment.feed?.jsonFeed?.[locale]) ?? [])));
44831
+ }
44832
+ async ngOnStart() {
44833
+ await this.settings.ready();
44834
+ console.debug(`${this._logPrefix}Service started`, this.settings.locale, this.feedUrls);
44835
+ return {
44836
+ locale: this.settings.locale,
44837
+ feedUrls: this.feedUrls,
44838
+ };
44839
+ }
44840
+ watchAll(opts) {
44841
+ if (!this.started) {
44842
+ return from(this.start()).pipe(switchMap$1(() => this.watchAll(opts)));
44843
+ }
44844
+ return this.feedUrls$.pipe(filter(isNotEmptyArray), mergeMap((urls) => this.loadAll(urls, opts)));
44845
+ }
44846
+ getHomeUrl(feed) {
44847
+ const feedUrl = feed?.feed_url ?? firstArrayValue(this.feedUrls);
44848
+ return feedUrl ? UrlUtils.getRootUrl(feedUrl) : undefined;
44849
+ }
44850
+ getTagUrl(feed, tag) {
44851
+ if (!feed || !tag)
44852
+ throw new Error("Missing 'feed' or 'tag' argument");
44853
+ const baseUrl = this.getHomeUrl(feed);
44854
+ return feed.tag_template?.replace('{tag}', tag) ?? baseUrl + '/tag/' + tag;
44855
+ }
44856
+ async load(url, opts) {
44857
+ const { data } = await this.loadAll([url], opts);
44858
+ return data?.[0];
44859
+ }
44860
+ async loadAll(urls, opts) {
44861
+ await this.ready();
44862
+ urls = urls ?? this.feedUrls;
44863
+ opts = {
44864
+ maxAgeInMonths: opts?.maxAgeInMonths ?? this.environment.feed?.maxAgeInMonths,
44865
+ maxContentLength: opts?.maxContentLength ?? this.environment.feed?.maxContentLength,
44866
+ locale: this.settings.locale,
44867
+ depth: 0,
44868
+ closeApis: true,
44869
+ ...opts,
44870
+ };
44871
+ const feeds = await Promise$1.all((urls || []).map(async (url) => {
44872
+ try {
44873
+ // Get JSON
44874
+ const json = await this.network.fetch(url);
44875
+ console.debug(`${this._logPrefix}Loaded JSON from ${url}`, json);
44876
+ if (JsonFeedUtils.isJsonFeed(json)) {
44877
+ // Migrate old version
44878
+ let feed = JsonFeedUtils.migrateFeed(json);
44879
+ // Resolve items (e.g. using feed.next_url)
44880
+ feed = await this.resolveFeedItems(feed, { ...opts, depth: opts?.depth ?? 0 });
44881
+ // Truncate html
44882
+ JsonFeedUtils.truncateFeedItemsHtml(feed, opts);
44883
+ return isNotEmptyArray(feed?.items) ? [feed] : [];
44884
+ }
44885
+ // Get discourse
44886
+ if (DiscourseUtils.isDiscourseObject(json)) {
44887
+ const api = this.getOrCreateDiscourseApi(url);
44888
+ return await DiscourseUtils.getFeedsByUrl(url, { ...opts, api });
44889
+ }
44890
+ return undefined;
44891
+ }
44892
+ catch (err) {
44893
+ if (err?.status === 404) {
44894
+ console.error(`${this._logPrefix} Error while fetching feed at ${url}. 404 (Not found)`);
44895
+ }
44896
+ else {
44897
+ console.error(`${this._logPrefix} Error while fetching feed at ${url}`, err);
44898
+ }
44899
+ return Promise$1.resolve([]);
44900
+ }
44901
+ }));
44902
+ if (opts?.closeApis !== false) {
44903
+ this.closeApis();
44904
+ }
44905
+ const data = feeds.flat();
44906
+ return { data, total: data.length };
44907
+ }
44908
+ getOrCreateDiscourseApi(url) {
44909
+ const baseUrl = DiscourseUtils.getDiscourseBaseUrl(url);
44910
+ let api = this.discourseApis.get(baseUrl);
44911
+ if (!api) {
44912
+ api = new DiscourseAPI(baseUrl);
44913
+ this.discourseApis.set(baseUrl, api);
44914
+ }
44915
+ return api;
44916
+ }
44917
+ closeApis() {
44918
+ this.discourseApis.clear();
44919
+ }
44920
+ async resolveFeedItems(json, opts) {
44921
+ if (!json)
44922
+ return json;
44923
+ // Load items
44924
+ if (isEmptyArray(json.items) && isNotNilOrBlank(json.next_url)) {
44925
+ const depth = opts?.depth ?? 0;
44926
+ if (depth >= 3) {
44927
+ console.warn(`${this._logPrefix}Max depth reached (${depth}) for ${json.feed_url}`);
44928
+ return json;
44929
+ }
44930
+ // Load items
44931
+ const feed = await this.load(json.next_url, { ...opts, closeApis: false, depth: depth + 1 });
44932
+ if (isEmptyArray(feed?.items)) {
44933
+ return json;
44934
+ }
44935
+ // Merge with the input json
44936
+ return {
44937
+ ...json,
44938
+ title: json.title ?? feed.title,
44939
+ home_page_url: json.home_page_url ?? feed.home_page_url,
44940
+ authors: isNotEmptyArray(json.authors) ? json.authors : feed.authors,
44941
+ items: feed.items,
44942
+ tag_template: json.tag_template ?? feed.tag_template,
44943
+ };
44944
+ }
44945
+ return json;
44946
+ }
44947
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DiscourseFeedService, deps: [{ token: LocalSettingsService }, { token: ENVIRONMENT, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
44948
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DiscourseFeedService, providedIn: 'root' });
44949
+ }
44950
+ __decorate([
44951
+ RxStateSelect()
44952
+ ], DiscourseFeedService.prototype, "locale$", void 0);
44953
+ __decorate([
44954
+ RxStateSelect()
44955
+ ], DiscourseFeedService.prototype, "feedUrls$", void 0);
44956
+ __decorate([
44957
+ RxStateProperty()
44958
+ ], DiscourseFeedService.prototype, "locale", void 0);
44959
+ __decorate([
44960
+ RxStateProperty()
44961
+ ], DiscourseFeedService.prototype, "feedUrls", void 0);
44962
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DiscourseFeedService, decorators: [{
44963
+ type: Injectable,
44964
+ args: [{ providedIn: 'root' }]
44965
+ }], ctorParameters: () => [{ type: LocalSettingsService }, { type: Environment, decorators: [{
44966
+ type: Optional
44967
+ }, {
44968
+ type: Inject,
44969
+ args: [ENVIRONMENT]
44970
+ }] }], propDecorators: { locale$: [], feedUrls$: [], locale: [], feedUrls: [] } });
44971
+
44299
44972
  const PersonFragments = {
44300
44973
  person: gql$1 `
44301
44974
  fragment PersonFragment on PersonVO {
@@ -49087,5 +49760,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImpo
49087
49760
  * Generated bundle index. Do not edit.
49088
49761
  */
49089
49762
 
49090
- export { APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOTKEYS_CONFIG, 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_NAMED_FILTER_SERVICE, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, 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, AppGraphQLModule, AppHomePageModule, AppIconComponent, AppIconModule, AppImageGalleryComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayPluckPipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_TESTING_PAGES, CapitalizePipe, CellValueChangeListener, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, 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, 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, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FileResponse, FileService, FileSizePipe, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, IMAGE_DEFAULTS, IPosition, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImagesUtils, InMemoryEntitiesService, 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, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, 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, 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, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NavActionsColumnComponent, 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, 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, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, 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_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedTextFormModule, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, compareVersionNumbers, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createPromiseEvent, createPromiseEventEmitter, 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, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hexToRgb, hexToRgbArray, initArrayControlsFromValues, interpolateString, intersectArrays, isAndroid, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isMacOS, isMobile, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isTouchUi, isVersionCompatible, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseLatitudeOrLongitude, propertiesPathComparator, propertyComparator, propertyPathComparator, referentialToString, referentialsToString, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, 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, toNotNil, toNumber, trimEmptyToNull, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
49763
+ export { APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOTKEYS_CONFIG, 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_NAMED_FILTER_SERVICE, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, 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, AppGraphQLModule, AppHomePageModule, AppIconComponent, AppIconModule, AppImageGalleryComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayPluckPipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_TESTING_PAGES, CapitalizePipe, CellValueChangeListener, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, 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, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DiscourseFeedService, DiscourseUtils, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FeedDirective, FeedModule, FeedsComponent, FileResponse, FileService, FileSizePipe, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, IMAGE_DEFAULTS, IPosition, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImagesUtils, InMemoryEntitiesService, 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, 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, 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, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NavActionsColumnComponent, 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, 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, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, 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_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedTextFormModule, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, compareVersionNumbers, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createPromiseEvent, createPromiseEventEmitter, 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, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hexToRgb, hexToRgbArray, initArrayControlsFromValues, interpolateString, intersectArrays, isAndroid, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isMacOS, isMobile, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isTouchUi, isVersionCompatible, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseLatitudeOrLongitude, propertiesPathComparator, propertyComparator, propertyPathComparator, referentialToString, referentialsToString, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, 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, toNotNil, toNumber, trimEmptyToNull, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
49091
49764
  //# sourceMappingURL=sumaris-net.ngx-components.mjs.map