@smartsoft001-mobilems/objects-shell-angular 2.129.0 → 2.130.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, contentChild, ChangeDetectionStrategy, Component, NgModule, PLATFORM_ID, isDevMode, Directive, signal, effect, viewChild, computed, untracked, input, output, linkedSignal } from '@angular/core';
2
+ import { Injectable, inject, contentChild, ChangeDetectionStrategy, Component, NgModule, PLATFORM_ID, isDevMode, Directive, signal, effect, viewChild, computed, untracked, input, output, linkedSignal, DestroyRef } from '@angular/core';
3
3
  import { FILE_SERVICE_CONFIG, PaginationMode, ListMode } from '@smartsoft001/angular';
4
4
  import { CrudFacade, initialState as initialState$1, getReducer as getReducer$1, CrudService, CrudConfig, CrudFullConfig, CrudCoreModule, CrudModule, CrudListPageBaseComponent, CrudItemPageBaseComponent } from '@smartsoft001/crud-shell-angular';
5
- import { CrudBaseService, FiltersBaseComponent, DICTIONARY_PAGE_SIZE, FileUrlService, provideInheritedSharedConfig, SharedModule, SeoService, TranslationService, MuseumSeoService, WcagChangesService, createSafeTimeouts, MasonryGridComponent, createCatalog, createCatalogLoadMore, toPlainText, namedEntryParts, serializeCatalogFilter, createCycleTagSearch, mergeSelectedTags, dictionaryFilterItems, dictionaryEntryKey, dictionaryEntries, SHARE_LINK_COPIED_TIMEOUT, ShareLinkBaseComponent } from '@smartsoft001-mobilems/angular';
5
+ import { CrudBaseService, FiltersBaseComponent, DICTIONARY_PAGE_SIZE, FileUrlService, provideInheritedSharedConfig, SharedModule, SeoService, TranslationService, MuseumSeoService, WcagChangesService, createSafeTimeouts, MasonryGridComponent, createCatalog, createCatalogLoadMore, toPlainText, namedEntryParts, serializeCatalogFilter, createCycleTagSearch, mergeSelectedTags, dictionaryFilterItems, dictionaryEntryKey, dictionaryEntries, SHARE_LINK_COPIED_TIMEOUT, ShareLinkBaseComponent, createLazyLoader } from '@smartsoft001-mobilems/angular';
6
6
  import { NgTemplateOutlet, CommonModule, isPlatformServer, isPlatformBrowser, DOCUMENT } from '@angular/common';
7
7
  import { firstValueFrom, retry, timer, throwError, map, tap, catchError, of, expand, reduce, switchMap, from, mergeMap, toArray, filter, takeUntil } from 'rxjs';
8
8
  import { createFeatureSelector } from '@ngrx/store';
@@ -10,6 +10,7 @@ import { toSignal, toObservable, rxResource } from '@angular/core/rxjs-interop';
10
10
  import { ActivatedRoute } from '@angular/router';
11
11
  import { TranslateService } from '@ngx-translate/core';
12
12
  import { capitalize } from '@smartsoft001/utils';
13
+ import { DomSanitizer } from '@angular/platform-browser';
13
14
 
14
15
  class ObjectsService extends CrudBaseService {
15
16
  getUrlNameForDetails() {
@@ -1741,9 +1742,1001 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImpor
1741
1742
  type: Directive
1742
1743
  }], propDecorators: { filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }] } });
1743
1744
 
1745
+ /**
1746
+ * Logic-only base of an object media tab (MOB-1801), from Muzeum Podlasia's
1747
+ * `components/tabs`: the `item` and `formFeatureTypes` inputs and `models` —
1748
+ * the object's `additionalImages` of the given form feature types, sorted by
1749
+ * `position`. The tab keeps its template, styles and `host`.
1750
+ *
1751
+ * `mediaTabFilterModels(models)` is a hook (identity by default) that a tab
1752
+ * overrides to narrow the list further.
1753
+ *
1754
+ * ```ts
1755
+ * export class OtherFilesTabComponent extends ObjectMediaTabBaseComponent<IAnimationObject> {}
1756
+ * ```
1757
+ */
1758
+ class ObjectMediaTabBaseComponent {
1759
+ constructor() {
1760
+ this.mediaTabFileUrl = inject(FileUrlService);
1761
+ this.mediaTabTranslate = inject(TranslateService);
1762
+ /** The object whose media the tab shows. */
1763
+ this.item = input.required(/* @ts-ignore */
1764
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
1765
+ /** The form feature types (`FormFeatureType`) the tab shows. */
1766
+ this.formFeatureTypes = input.required(/* @ts-ignore */
1767
+ ...(ngDevMode ? [{ debugName: "formFeatureTypes" }] : /* istanbul ignore next */ []));
1768
+ /** The object's media of `formFeatureTypes`, sorted by `position`, then through `mediaTabFilterModels`. */
1769
+ this.models = computed(() => {
1770
+ const obj = this.item();
1771
+ let filtered = [];
1772
+ if (obj.additionalImages && Array.isArray(obj.additionalImages)) {
1773
+ filtered = obj.additionalImages
1774
+ .filter((img) => this.formFeatureTypes().includes(img?.formFeature))
1775
+ .sort((a, b) => (a?.position ?? 0) - (b?.position ?? 0));
1776
+ }
1777
+ return this.mediaTabFilterModels(filtered);
1778
+ }, /* @ts-ignore */
1779
+ ...(ngDevMode ? [{ debugName: "models" }] : /* istanbul ignore next */ []));
1780
+ }
1781
+ /** Narrows the sorted list. Identity by default. */
1782
+ mediaTabFilterModels(models) {
1783
+ return models;
1784
+ }
1785
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectMediaTabBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1786
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: ObjectMediaTabBaseComponent, isStandalone: true, inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, formFeatureTypes: { classPropertyName: "formFeatureTypes", publicName: "formFeatureTypes", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 }); }
1787
+ }
1788
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectMediaTabBaseComponent, decorators: [{
1789
+ type: Directive
1790
+ }], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], formFeatureTypes: [{ type: i0.Input, args: [{ isSignal: true, alias: "formFeatureTypes", required: true }] }] } });
1791
+
1792
+ /**
1793
+ * Logic-only base of the media tabs' download button, from Muzeum Podlasia's
1794
+ * `download-button`: `download()` fetches `url`, saves the blob as `filename`
1795
+ * through a temporary `<a download>` and revokes the object URL. It runs only
1796
+ * from a click, so never on the server. The button keeps its template.
1797
+ *
1798
+ * ```ts
1799
+ * export class DownloadButtonComponent extends ObjectDownloadButtonBaseComponent {}
1800
+ * ```
1801
+ */
1802
+ class ObjectDownloadButtonBaseComponent {
1803
+ constructor() {
1804
+ this.downloadButtonDocument = inject(DOCUMENT);
1805
+ /** The file URL. An empty URL does nothing. */
1806
+ this.url = input.required(/* @ts-ignore */
1807
+ ...(ngDevMode ? [{ debugName: "url" }] : /* istanbul ignore next */ []));
1808
+ /** The name the file is saved under. */
1809
+ this.filename = input.required(/* @ts-ignore */
1810
+ ...(ngDevMode ? [{ debugName: "filename" }] : /* istanbul ignore next */ []));
1811
+ /** `true` while the file is being fetched. */
1812
+ this.isDownloading = signal(false, /* @ts-ignore */
1813
+ ...(ngDevMode ? [{ debugName: "isDownloading" }] : /* istanbul ignore next */ []));
1814
+ }
1815
+ async download() {
1816
+ const url = this.url();
1817
+ const filename = this.filename();
1818
+ if (!url)
1819
+ return;
1820
+ this.isDownloading.set(true);
1821
+ try {
1822
+ const response = await fetch(url);
1823
+ const blob = await response.blob();
1824
+ const blobUrl = URL.createObjectURL(blob);
1825
+ const link = this.downloadButtonDocument.createElement('a');
1826
+ link.href = blobUrl;
1827
+ link.download = filename;
1828
+ link.click();
1829
+ URL.revokeObjectURL(blobUrl);
1830
+ }
1831
+ catch (error) {
1832
+ console.error('Failed to download file:', error);
1833
+ }
1834
+ finally {
1835
+ this.isDownloading.set(false);
1836
+ }
1837
+ }
1838
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectDownloadButtonBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1839
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: ObjectDownloadButtonBaseComponent, isStandalone: true, inputs: { url: { classPropertyName: "url", publicName: "url", isSignal: true, isRequired: true, transformFunction: null }, filename: { classPropertyName: "filename", publicName: "filename", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 }); }
1840
+ }
1841
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectDownloadButtonBaseComponent, decorators: [{
1842
+ type: Directive
1843
+ }], propDecorators: { url: [{ type: i0.Input, args: [{ isSignal: true, alias: "url", required: true }] }], filename: [{ type: i0.Input, args: [{ isSignal: true, alias: "filename", required: true }] }] } });
1844
+
1845
+ /**
1846
+ * Logic-only base of the object map tab, from Muzeum Podlasia's `map-tab`:
1847
+ * the object's `archeoMap` coordinates. The tab keeps its template (the map
1848
+ * component and its zoom).
1849
+ *
1850
+ * ```ts
1851
+ * export class MapTabComponent extends ObjectMapTabBaseComponent {}
1852
+ * ```
1853
+ */
1854
+ class ObjectMapTabBaseComponent {
1855
+ constructor() {
1856
+ /** The object whose `archeoMap` the tab shows. */
1857
+ this.item = input.required(/* @ts-ignore */
1858
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
1859
+ this.mapTabArcheoMap = computed(() => {
1860
+ const obj = this.item();
1861
+ return obj?.archeoMap;
1862
+ }, /* @ts-ignore */
1863
+ ...(ngDevMode ? [{ debugName: "mapTabArcheoMap" }] : /* istanbul ignore next */ []));
1864
+ /** Truthy (the longitude string) when both coordinates are set. */
1865
+ this.hasCoordinates = computed(() => {
1866
+ const map = this.mapTabArcheoMap();
1867
+ return map?.latitude && map?.longitude;
1868
+ }, /* @ts-ignore */
1869
+ ...(ngDevMode ? [{ debugName: "hasCoordinates" }] : /* istanbul ignore next */ []));
1870
+ /** `parseFloat(latitude)`, `0` when missing. */
1871
+ this.latitude = computed(() => {
1872
+ const map = this.mapTabArcheoMap();
1873
+ return map?.latitude ? parseFloat(map.latitude) : 0;
1874
+ }, /* @ts-ignore */
1875
+ ...(ngDevMode ? [{ debugName: "latitude" }] : /* istanbul ignore next */ []));
1876
+ /** `parseFloat(longitude)`, `0` when missing. */
1877
+ this.longitude = computed(() => {
1878
+ const map = this.mapTabArcheoMap();
1879
+ return map?.longitude ? parseFloat(map.longitude) : 0;
1880
+ }, /* @ts-ignore */
1881
+ ...(ngDevMode ? [{ debugName: "longitude" }] : /* istanbul ignore next */ []));
1882
+ }
1883
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectMapTabBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1884
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: ObjectMapTabBaseComponent, isStandalone: true, inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 }); }
1885
+ }
1886
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectMapTabBaseComponent, decorators: [{
1887
+ type: Directive
1888
+ }], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }] } });
1889
+
1890
+ /**
1891
+ * Logic-only base of an object tab with native `<audio>` players (MOB-1801):
1892
+ * the file URL, title, aria label and download file name of each recording,
1893
+ * shared by `ObjectAudioTabBaseComponent` and
1894
+ * `ObjectAudioDescriptionTabBaseComponent`. The fallback label comes from
1895
+ * `audioMediaTabLabelKey`, read lazily, so a subclass overrides it with a
1896
+ * field.
1897
+ *
1898
+ * ```ts
1899
+ * protected override readonly audioMediaTabLabelKey = 'DETAILS_PAGE.tabs.audio';
1900
+ * ```
1901
+ */
1902
+ class ObjectAudioMediaTabBaseComponent extends ObjectMediaTabBaseComponent {
1903
+ constructor() {
1904
+ super(...arguments);
1905
+ /** Translation key of the player label; read lazily, so a subclass can override it. */
1906
+ this.audioMediaTabLabelKey = 'OBJECTS.filters.multimedia.audio';
1907
+ }
1908
+ /** The recording's file URL (`noCache`). */
1909
+ getAudioUrl(audio) {
1910
+ return this.mediaTabFileUrl.get(audio, 'noCache');
1911
+ }
1912
+ /** Alt text in the current language, then the description, then the label key. */
1913
+ getAudioTitle(audio) {
1914
+ const currentLang = this.mediaTabTranslate.currentLang;
1915
+ const altText = currentLang === 'en' ? audio.altTextEn : audio.altText;
1916
+ return (altText ||
1917
+ audio.description ||
1918
+ this.mediaTabTranslate.instant(this.audioMediaTabLabelKey));
1919
+ }
1920
+ /** `<label>: <title>`. */
1921
+ getAudioAriaLabel(audio) {
1922
+ const title = this.getAudioTitle(audio);
1923
+ const audioLabel = this.mediaTabTranslate.instant(this.audioMediaTabLabelKey);
1924
+ return `${audioLabel}: ${title}`;
1925
+ }
1926
+ /** The object title, alt text, description or `audio`, plus the extension (`mp3` by default). */
1927
+ getDownloadFilename(audio) {
1928
+ const name = this.item()?.title || audio.altText || audio.description || 'audio';
1929
+ const ext = audio.extension || 'mp3';
1930
+ return `${name}.${ext}`;
1931
+ }
1932
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectAudioMediaTabBaseComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
1933
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: ObjectAudioMediaTabBaseComponent, isStandalone: true, usesInheritance: true, ngImport: i0 }); }
1934
+ }
1935
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectAudioMediaTabBaseComponent, decorators: [{
1936
+ type: Directive
1937
+ }] });
1938
+
1939
+ /**
1940
+ * Logic-only base of the object audio tab (MOB-1801). Muzeum Podlasia renders
1941
+ * the article player (`app-widget-audio` on `WidgetAudioBaseComponent`,
1942
+ * MOB-1805 / MOB-1872) through `widgetItem` and `trackName`; sztuki-2 and the
1943
+ * template render a native `<audio>` with a download button through
1944
+ * `getAudioUrl`, `getAudioTitle`, `getAudioAriaLabel` and
1945
+ * `getDownloadFilename` (`ObjectAudioMediaTabBaseComponent`, label
1946
+ * `DETAILS_PAGE.tabs.audio`), with `playableOnly: false`. The tab keeps its
1947
+ * template, styles and `host`.
1948
+ *
1949
+ * ```ts
1950
+ * export class AudioTabComponent extends ObjectAudioTabBaseComponent {
1951
+ * constructor() {
1952
+ * super(); // sztuki-2 / template: super({ playableOnly: false })
1953
+ * }
1954
+ * }
1955
+ * ```
1956
+ */
1957
+ class ObjectAudioTabBaseComponent extends ObjectAudioMediaTabBaseComponent {
1958
+ constructor(options = {}) {
1959
+ super();
1960
+ this.audioMediaTabLabelKey = 'DETAILS_PAGE.tabs.audio';
1961
+ this.audioTabOptions = options;
1962
+ }
1963
+ /** With `playableOnly` (default) only recordings with a file. */
1964
+ mediaTabFilterModels(models) {
1965
+ if (this.audioTabOptions.playableOnly === false)
1966
+ return models;
1967
+ // Only recordings with a file — the "Audio 1, 2…" numbering counts what
1968
+ // is shown (MOB-1872 review).
1969
+ return models.filter((audio) => !!audio.filePath);
1970
+ }
1971
+ /**
1972
+ * The recording in the audio widget's shape: the file in `file`, the
1973
+ * heading from the description (without one — a player without a heading,
1974
+ * as in the mock-up).
1975
+ */
1976
+ widgetItem(audio) {
1977
+ return {
1978
+ file: audio,
1979
+ title: audio.description ?? '',
1980
+ };
1981
+ }
1982
+ /**
1983
+ * The recording's name for screen readers (the player's button and
1984
+ * slider): the alt text in the current language or the description;
1985
+ * several recordings without names — "Audio 1", "Audio 2"… (MOB-1872
1986
+ * review); a single one — `null`.
1987
+ */
1988
+ trackName(audio, index) {
1989
+ const altText = this.mediaTabTranslate.currentLang === 'en'
1990
+ ? audio.altTextEn
1991
+ : audio.altText;
1992
+ const name = (altText || audio.description || '').trim();
1993
+ if (name)
1994
+ return name;
1995
+ return this.models().length > 1
1996
+ ? `${this.mediaTabTranslate.instant(this.audioMediaTabLabelKey)} ${index + 1}`
1997
+ : null;
1998
+ }
1999
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectAudioTabBaseComponent, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive }); }
2000
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: ObjectAudioTabBaseComponent, isStandalone: true, usesInheritance: true, ngImport: i0 }); }
2001
+ }
2002
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectAudioTabBaseComponent, decorators: [{
2003
+ type: Directive
2004
+ }], ctorParameters: () => [{ type: undefined }] });
2005
+
2006
+ /**
2007
+ * Logic-only base of the object audio description tab (MOB-1801), from Muzeum
2008
+ * Podlasia's `audio-description-tab.component.ts` (sztuki-2 and the template
2009
+ * differ by one template class only): the main image above the players and
2010
+ * the names of each recording (`ObjectAudioMediaTabBaseComponent`, label
2011
+ * `OBJECTS.filters.multimedia.audio`). The tab keeps its template, styles and `host`.
2012
+ *
2013
+ * ```ts
2014
+ * export class AudioDescriptionTabComponent extends ObjectAudioDescriptionTabBaseComponent {}
2015
+ * ```
2016
+ */
2017
+ class ObjectAudioDescriptionTabBaseComponent extends ObjectAudioMediaTabBaseComponent {
2018
+ constructor() {
2019
+ super(...arguments);
2020
+ /** The object's main image (`big` = full resolution without cache); `null` without one or for the no-image placeholder. */
2021
+ this.mainImageUrl = computed(() => {
2022
+ const obj = this.item();
2023
+ if (obj?.image) {
2024
+ const url = this.mediaTabFileUrl.get(obj.image, 'big');
2025
+ if (url && !url.includes('assets/no-image.svg')) {
2026
+ return url;
2027
+ }
2028
+ }
2029
+ return null;
2030
+ }, /* @ts-ignore */
2031
+ ...(ngDevMode ? [{ debugName: "mainImageUrl" }] : /* istanbul ignore next */ []));
2032
+ }
2033
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectAudioDescriptionTabBaseComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
2034
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: ObjectAudioDescriptionTabBaseComponent, isStandalone: true, usesInheritance: true, ngImport: i0 }); }
2035
+ }
2036
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectAudioDescriptionTabBaseComponent, decorators: [{
2037
+ type: Directive
2038
+ }] });
2039
+
2040
+ /**
2041
+ * Logic-only base of the object documents tab (MOB-1801), from Muzeum
2042
+ * Podlasia's `documents-tab.component.ts` (sztuki-2 and the template differ by
2043
+ * one template class only): each document's URL, display name and download
2044
+ * file name. The tab keeps its template, styles and `host`.
2045
+ *
2046
+ * ```ts
2047
+ * export class DocumentsTabComponent extends ObjectDocumentsTabBaseComponent {}
2048
+ * ```
2049
+ */
2050
+ class ObjectDocumentsTabBaseComponent extends ObjectMediaTabBaseComponent {
2051
+ /** The document's file URL (`noCache`). */
2052
+ getDocumentUrl(doc) {
2053
+ return this.mediaTabFileUrl.get(doc, 'noCache');
2054
+ }
2055
+ /** Alt text in the current language, then the description, then the upper-case extension (`PDF` by default). */
2056
+ getDocumentDisplayName(doc) {
2057
+ const currentLang = this.mediaTabTranslate.currentLang;
2058
+ const altText = currentLang === 'en' ? doc.altTextEn : doc.altText;
2059
+ if (altText)
2060
+ return altText;
2061
+ if (doc.description)
2062
+ return doc.description;
2063
+ // Show the extension name when there is no description
2064
+ const extension = doc.extension || 'pdf';
2065
+ return extension.toUpperCase();
2066
+ }
2067
+ /** Alt text in the current language, description or `document`, plus the extension (`pdf` by default). */
2068
+ getDocumentFileName(doc) {
2069
+ const currentLang = this.mediaTabTranslate.currentLang;
2070
+ const altText = currentLang === 'en' ? doc.altTextEn : doc.altText;
2071
+ const name = altText || doc.description || 'document';
2072
+ const extension = doc.extension || 'pdf';
2073
+ return `${name}.${extension}`;
2074
+ }
2075
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectDocumentsTabBaseComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
2076
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: ObjectDocumentsTabBaseComponent, isStandalone: true, usesInheritance: true, ngImport: i0 }); }
2077
+ }
2078
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectDocumentsTabBaseComponent, decorators: [{
2079
+ type: Directive
2080
+ }] });
2081
+
2082
+ /**
2083
+ * Creates the selector state of a media tab, 1:1 from Muzeum Podlasia's
2084
+ * animation tab. `fallbackLabel` gets the 0-based index of a model without
2085
+ * `altText` and `description`. When `item()` becomes a different object (a new
2086
+ * `id`), the first model is selected; an object that only re-emits keeps the
2087
+ * user's choice. `item` and `models` are functions (`() => this.item()`), read
2088
+ * lazily, so a tab that redeclares `item` still works. Call it in an injection context (a field initializer),
2089
+ * because it registers an `effect`.
2090
+ */
2091
+ function createMediaTabPicker(item, models, fallbackLabel) {
2092
+ const selectedKey = signal(null, /* @ts-ignore */
2093
+ ...(ngDevMode ? [{ debugName: "selectedKey" }] : /* istanbul ignore next */ []));
2094
+ const options = computed(() => {
2095
+ return models().map((model, index) => ({
2096
+ key: String(model.id),
2097
+ value: model?.altText ?? model?.description ?? fallbackLabel(index),
2098
+ }));
2099
+ }, /* @ts-ignore */
2100
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
2101
+ const selected = computed(() => {
2102
+ const list = models();
2103
+ if (list.length === 0)
2104
+ return null;
2105
+ const key = selectedKey();
2106
+ if (key) {
2107
+ const found = list.find((model) => String(model.id) === key);
2108
+ if (found)
2109
+ return found;
2110
+ }
2111
+ return list[0];
2112
+ }, /* @ts-ignore */
2113
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
2114
+ const selectedOption = computed(() => {
2115
+ const model = selected();
2116
+ const list = options();
2117
+ if (model) {
2118
+ const found = list.find((option) => option.key === String(model.id));
2119
+ if (found)
2120
+ return found;
2121
+ }
2122
+ return list[0] || { key: '', value: '' };
2123
+ }, /* @ts-ignore */
2124
+ ...(ngDevMode ? [{ debugName: "selectedOption" }] : /* istanbul ignore next */ []));
2125
+ let lastItemId = null;
2126
+ effect(() => {
2127
+ const currentItem = item();
2128
+ const list = models();
2129
+ // Only reset the selected key when the item changes (a different object)
2130
+ if (currentItem?.id !== lastItemId) {
2131
+ lastItemId = currentItem?.id ?? null;
2132
+ if (list.length > 0) {
2133
+ selectedKey.set(String(list[0].id));
2134
+ }
2135
+ else {
2136
+ selectedKey.set(null);
2137
+ }
2138
+ }
2139
+ });
2140
+ return { selectedKey, options, selected, selectedOption };
2141
+ }
2142
+
2143
+ /**
2144
+ * Logic-only base of an object tab with one video at a time and a dropdown
2145
+ * (MOB-1801), shared by `ObjectAnimationTabBaseComponent` and
2146
+ * `ObjectVideoTabBaseComponent`, 1:1 from Muzeum Podlasia's animation and
2147
+ * video tabs: the selector state, the player reset on a change and the
2148
+ * player helpers. The translation keys and the download fallback name are
2149
+ * abstract fields, read lazily.
2150
+ */
2151
+ class ObjectVideoMediaTabBaseComponent extends ObjectMediaTabBaseComponent {
2152
+ constructor() {
2153
+ super(...arguments);
2154
+ /** The template's native `<video #videoPlayer>`, if it has one. */
2155
+ this.videoPlayer = viewChild('videoPlayer', /* @ts-ignore */
2156
+ ...(ngDevMode ? [{ debugName: "videoPlayer" }] : /* istanbul ignore next */ []));
2157
+ this.videoMediaTabPicker = createMediaTabPicker(() => this.item(), () => this.models(), (index) => `${this.mediaTabTranslate.instant(this.videoMediaTabOptionLabelKey)} ${index + 1}`);
2158
+ /** The chosen video's key; the first video of each new object. */
2159
+ this.selectedVideoKey = this.videoMediaTabPicker.selectedKey;
2160
+ /** Dropdown options: alt text, description or `<label> <n>`. */
2161
+ this.videoOptions = this.videoMediaTabPicker.options;
2162
+ /** The chosen video, the first one by default. */
2163
+ this.selectedVideo = this.videoMediaTabPicker.selected;
2164
+ /** The dropdown option of `selectedVideo`. */
2165
+ this.selectedVideoOption = this.videoMediaTabPicker.selectedOption;
2166
+ }
2167
+ /** Selects a video; a native `#videoPlayer` is paused and rewound. */
2168
+ videoChange(key) {
2169
+ this.selectedVideoKey.set(key);
2170
+ // Reset video playback
2171
+ const player = this.videoPlayer()?.nativeElement;
2172
+ if (player) {
2173
+ player.pause();
2174
+ player.currentTime = 0;
2175
+ }
2176
+ }
2177
+ /** The video's file URL (`noCache`). */
2178
+ getVideoUrl(video) {
2179
+ return this.mediaTabFileUrl.get(video, 'noCache');
2180
+ }
2181
+ /** Alt text in the current language, then the description, then the title key. */
2182
+ getVideoTitle(video) {
2183
+ const currentLang = this.mediaTabTranslate.currentLang;
2184
+ const altText = currentLang === 'en' ? video.altTextEn : video.altText;
2185
+ return (altText ||
2186
+ video.description ||
2187
+ this.mediaTabTranslate.instant(this.videoMediaTabTitleKey));
2188
+ }
2189
+ /** `<title key>: <title>`. */
2190
+ getVideoAriaLabel(video) {
2191
+ const title = this.getVideoTitle(video);
2192
+ const videoLabel = this.mediaTabTranslate.instant(this.videoMediaTabTitleKey);
2193
+ return `${videoLabel}: ${title}`;
2194
+ }
2195
+ /** The object title, alt text, description or the fallback name, plus the extension (`mp4` by default). */
2196
+ getDownloadFilename(video) {
2197
+ const name = this.item()?.title ||
2198
+ video.altText ||
2199
+ video.description ||
2200
+ this.videoMediaTabDownloadName;
2201
+ const ext = video.extension || 'mp4';
2202
+ return `${name}.${ext}`;
2203
+ }
2204
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectVideoMediaTabBaseComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
2205
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.3", type: ObjectVideoMediaTabBaseComponent, isStandalone: true, viewQueries: [{ propertyName: "videoPlayer", first: true, predicate: ["videoPlayer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0 }); }
2206
+ }
2207
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectVideoMediaTabBaseComponent, decorators: [{
2208
+ type: Directive
2209
+ }], propDecorators: { videoPlayer: [{ type: i0.ViewChild, args: ['videoPlayer', { isSignal: true }] }] } });
2210
+
2211
+ /**
2212
+ * Logic-only base of the object animation tab (MOB-1801), 1:1 from Muzeum
2213
+ * Podlasia (sztuki-2 and the template are identical): options without a name
2214
+ * are `OBJECTS.filters.multimedia.animation <n>`, the player is titled with
2215
+ * `OBJECTS.filters.multimedia.video`, the download falls back to
2216
+ * `animation.mp4`, and `videoChange` pauses and rewinds `#videoPlayer`. The
2217
+ * tab keeps its template, styles and `host`.
2218
+ *
2219
+ * ```ts
2220
+ * export class AnimationTabComponent extends ObjectAnimationTabBaseComponent {}
2221
+ * ```
2222
+ */
2223
+ class ObjectAnimationTabBaseComponent extends ObjectVideoMediaTabBaseComponent {
2224
+ constructor() {
2225
+ super(...arguments);
2226
+ this.videoMediaTabOptionLabelKey = 'OBJECTS.filters.multimedia.animation';
2227
+ this.videoMediaTabTitleKey = 'OBJECTS.filters.multimedia.video';
2228
+ this.videoMediaTabDownloadName = 'animation';
2229
+ }
2230
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectAnimationTabBaseComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
2231
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: ObjectAnimationTabBaseComponent, isStandalone: true, usesInheritance: true, ngImport: i0 }); }
2232
+ }
2233
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectAnimationTabBaseComponent, decorators: [{
2234
+ type: Directive
2235
+ }] });
2236
+
2237
+ /**
2238
+ * Logic-only base of the object video tab (MOB-1801). Options, title and
2239
+ * aria label use `DETAILS_PAGE.tabs.video`. Muzeum Podlasia renders the app's
2240
+ * video player (a new source resets it — a still frame with a play button,
2241
+ * no playback), so its template has no `#videoPlayer` and `videoChange` only
2242
+ * selects the video. Sztuki-2 and the template render a native
2243
+ * `<video #videoPlayer>`, which `videoChange` pauses and rewinds, and a
2244
+ * download button named by `getDownloadFilename` (fallback `video.mp4`). The
2245
+ * tab keeps its template, styles and `host`.
2246
+ *
2247
+ * ```ts
2248
+ * export class VideoTabComponent extends ObjectVideoTabBaseComponent {}
2249
+ * ```
2250
+ */
2251
+ class ObjectVideoTabBaseComponent extends ObjectVideoMediaTabBaseComponent {
2252
+ constructor() {
2253
+ super(...arguments);
2254
+ this.videoMediaTabOptionLabelKey = 'DETAILS_PAGE.tabs.video';
2255
+ this.videoMediaTabTitleKey = 'DETAILS_PAGE.tabs.video';
2256
+ this.videoMediaTabDownloadName = 'video';
2257
+ }
2258
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectVideoTabBaseComponent, deps: null, target: i0.ɵɵFactoryTarget.Directive }); }
2259
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: ObjectVideoTabBaseComponent, isStandalone: true, usesInheritance: true, ngImport: i0 }); }
2260
+ }
2261
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectVideoTabBaseComponent, decorators: [{
2262
+ type: Directive
2263
+ }] });
2264
+
2265
+ /**
2266
+ * Logic-only base of the object virtual walk tab (MOB-1801), 1:1 from Muzeum
2267
+ * Podlasia (sztuki-2 and the template are identical): the walk dropdown, the
2268
+ * trusted iframe source and the fullscreen toggle of `#walkContainer`. The
2269
+ * `fullscreenchange` listener is registered in the browser only, through the
2270
+ * injected `DOCUMENT`, and removed on destroy. The tab keeps its template,
2271
+ * styles and `host`.
2272
+ *
2273
+ * ```ts
2274
+ * export class VirtualWalkTabComponent extends ObjectVirtualWalkTabBaseComponent {
2275
+ * constructor() {
2276
+ * super();
2277
+ * }
2278
+ * }
2279
+ * ```
2280
+ */
2281
+ class ObjectVirtualWalkTabBaseComponent extends ObjectMediaTabBaseComponent {
2282
+ constructor() {
2283
+ super();
2284
+ this.virtualWalkTabSanitizer = inject(DomSanitizer);
2285
+ this.virtualWalkTabDocument = inject(DOCUMENT);
2286
+ /** The template's `#iframeContainer`. */
2287
+ this.iframeContainer = viewChild('iframeContainer', /* @ts-ignore */
2288
+ ...(ngDevMode ? [{ debugName: "iframeContainer" }] : /* istanbul ignore next */ []));
2289
+ /** The template's `#walkContainer`, the element shown fullscreen. */
2290
+ this.walkContainer = viewChild('walkContainer', /* @ts-ignore */
2291
+ ...(ngDevMode ? [{ debugName: "walkContainer" }] : /* istanbul ignore next */ []));
2292
+ this.virtualWalkTabPicker = createMediaTabPicker(() => this.item(), () => this.models(), (index) => `${this.mediaTabTranslate.instant('OBJECTS.filters.multimedia.virtualWalk')} ${index + 1}`);
2293
+ /** The chosen walk's key; the first walk of each new object. */
2294
+ this.selectedWalkKey = this.virtualWalkTabPicker.selectedKey;
2295
+ /** Dropdown options: alt text, description or `OBJECTS.filters.multimedia.virtualWalk <n>`. */
2296
+ this.walkOptions = this.virtualWalkTabPicker.options;
2297
+ /** The chosen walk, the first one by default. */
2298
+ this.selectedWalk = this.virtualWalkTabPicker.selected;
2299
+ /** The dropdown option of `selectedWalk`. */
2300
+ this.selectedWalkOption = this.virtualWalkTabPicker.selectedOption;
2301
+ /** The selected walk's `iframeSource`, trusted; `null` without one. */
2302
+ this.safeIframeUrl = computed(() => {
2303
+ const walk = this.selectedWalk();
2304
+ if (!walk?.iframeSource)
2305
+ return null;
2306
+ return this.virtualWalkTabSanitizer.bypassSecurityTrustResourceUrl(walk.iframeSource);
2307
+ }, /* @ts-ignore */
2308
+ ...(ngDevMode ? [{ debugName: "safeIframeUrl" }] : /* istanbul ignore next */ []));
2309
+ /** Whether the document is in fullscreen (follows `fullscreenchange`, e.g. Escape). */
2310
+ this.isFullscreen = signal(false, /* @ts-ignore */
2311
+ ...(ngDevMode ? [{ debugName: "isFullscreen" }] : /* istanbul ignore next */ []));
2312
+ if (isPlatformBrowser(inject(PLATFORM_ID))) {
2313
+ // Listen for fullscreen changes (e.g. when the user presses Escape)
2314
+ const doc = this.virtualWalkTabDocument;
2315
+ const onFullscreenChange = () => {
2316
+ this.isFullscreen.set(!!doc.fullscreenElement);
2317
+ };
2318
+ doc.addEventListener('fullscreenchange', onFullscreenChange);
2319
+ inject(DestroyRef).onDestroy(() => {
2320
+ doc.removeEventListener('fullscreenchange', onFullscreenChange);
2321
+ });
2322
+ }
2323
+ }
2324
+ /** Selects a walk. */
2325
+ walkChange(key) {
2326
+ this.selectedWalkKey.set(key);
2327
+ }
2328
+ /** Alt text in the current language, then the description, then `OBJECTS.filters.multimedia.virtualWalk`. */
2329
+ getWalkTitle(walk) {
2330
+ const currentLang = this.mediaTabTranslate.currentLang;
2331
+ const altText = currentLang === 'en' ? walk.altTextEn : walk.altText;
2332
+ return (altText ||
2333
+ walk.description ||
2334
+ this.mediaTabTranslate.instant('OBJECTS.filters.multimedia.virtualWalk'));
2335
+ }
2336
+ /** Shows `#walkContainer` fullscreen, or leaves fullscreen. */
2337
+ toggleFullscreen() {
2338
+ const container = this.walkContainer();
2339
+ if (!container)
2340
+ return;
2341
+ const element = container.nativeElement;
2342
+ const doc = this.virtualWalkTabDocument;
2343
+ if (!doc.fullscreenElement) {
2344
+ element.requestFullscreen().catch((err) => {
2345
+ console.error('Error attempting to enable fullscreen:', err);
2346
+ });
2347
+ }
2348
+ else {
2349
+ doc.exitFullscreen();
2350
+ }
2351
+ }
2352
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectVirtualWalkTabBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2353
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.3", type: ObjectVirtualWalkTabBaseComponent, isStandalone: true, viewQueries: [{ propertyName: "iframeContainer", first: true, predicate: ["iframeContainer"], descendants: true, isSignal: true }, { propertyName: "walkContainer", first: true, predicate: ["walkContainer"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0 }); }
2354
+ }
2355
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectVirtualWalkTabBaseComponent, decorators: [{
2356
+ type: Directive
2357
+ }], ctorParameters: () => [], propDecorators: { iframeContainer: [{ type: i0.ViewChild, args: ['iframeContainer', { isSignal: true }] }], walkContainer: [{ type: i0.ViewChild, args: ['walkContainer', { isSignal: true }] }] } });
2358
+
2359
+ /**
2360
+ * Logic-only base of the object 3D model tab (MOB-1801), 1:1 from Muzeum
2361
+ * Podlasia: the model list, the lazily loaded `online-3d-viewer`, the model
2362
+ * switch, the camera reset, the wireframe and the fullscreen toggle. The tab
2363
+ * keeps its template, styles and `host`, and returns its own
2364
+ * `import('online-3d-viewer')` from `loadModel3dLibrary()` — the framework does
2365
+ * not depend on the library, and the viewer is never loaded on the server.
2366
+ * Podlasie's model file URI panel (MOB-981) stays in Podlasie and follows
2367
+ * `selectedModel`.
2368
+ *
2369
+ * ```ts
2370
+ * export class Model3dTabComponent extends ObjectModel3dTabBaseComponent {
2371
+ * constructor() {
2372
+ * super(); // sztuki-2 / template: super({ loadableOnly: false })
2373
+ * }
2374
+ *
2375
+ * protected loadModel3dLibrary() {
2376
+ * return import('online-3d-viewer');
2377
+ * }
2378
+ * }
2379
+ * ```
2380
+ */
2381
+ class ObjectModel3dTabBaseComponent extends ObjectMediaTabBaseComponent {
2382
+ constructor(options = {}) {
2383
+ super();
2384
+ /** The template's `#viewer3d`, the viewer's canvas container. */
2385
+ this.viewer3d = viewChild('viewer3d', /* @ts-ignore */
2386
+ ...(ngDevMode ? [{ debugName: "viewer3d" }] : /* istanbul ignore next */ []));
2387
+ /** The chosen model's index, bound with `ngModel` (may arrive as a string). */
2388
+ this.selectedModelIndex = 0;
2389
+ /** The chosen model's index as a signal, next to `selectedModelIndex` (`onModelChange`). */
2390
+ this.selectedModel = signal(0, /* @ts-ignore */
2391
+ ...(ngDevMode ? [{ debugName: "selectedModel" }] : /* istanbul ignore next */ []));
2392
+ /** A model is loading. */
2393
+ this.isLoading = signal(false, /* @ts-ignore */
2394
+ ...(ngDevMode ? [{ debugName: "isLoading" }] : /* istanbul ignore next */ []));
2395
+ /** The load error, `null` without one. */
2396
+ this.error = signal(null, /* @ts-ignore */
2397
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
2398
+ /** The edges are shown. */
2399
+ this.isWireframe = signal(false, /* @ts-ignore */
2400
+ ...(ngDevMode ? [{ debugName: "isWireframe" }] : /* istanbul ignore next */ []));
2401
+ /** Whether the document is in fullscreen (follows `fullscreenchange`, e.g. Escape). */
2402
+ this.isFullscreen = signal(false, /* @ts-ignore */
2403
+ ...(ngDevMode ? [{ debugName: "isFullscreen" }] : /* istanbul ignore next */ []));
2404
+ this.model3dTabDocument = inject(DOCUMENT);
2405
+ this.model3dTabPlatformId = inject(PLATFORM_ID);
2406
+ this.model3dTabTimeouts = createSafeTimeouts();
2407
+ /**
2408
+ * `online-3d-viewer` (+ `three`, ~1.1 MB) is loaded only on the first
2409
+ * init — a static import put the library in the chunk of the whole objects
2410
+ * module, loaded on the `/obiekty` list and every card, also without a 3D
2411
+ * model (Lighthouse, MOB-1850, MOB-1895). One load is shared between
2412
+ * concurrent calls. Created on first use.
2413
+ */
2414
+ this.model3dTabLibraryLoader = null;
2415
+ /** The number of the latest init — an older call after `await` does not overwrite a newer one. */
2416
+ this.model3dTabInitToken = 0;
2417
+ this.model3dTabViewer = null;
2418
+ this.model3dTabOptions = options;
2419
+ effect(() => {
2420
+ const viewerElement = this.viewer3d();
2421
+ const models = this.models();
2422
+ if (viewerElement && models.length > 0) {
2423
+ this.model3dTabTimeouts.later(() => void this.model3dTabInitViewer(), 100);
2424
+ }
2425
+ });
2426
+ if (isPlatformBrowser(this.model3dTabPlatformId)) {
2427
+ // Listen for fullscreen changes (e.g. when the user presses Escape)
2428
+ const doc = this.model3dTabDocument;
2429
+ const onFullscreenChange = () => {
2430
+ const wasFullscreen = this.isFullscreen();
2431
+ this.isFullscreen.set(!!doc.fullscreenElement);
2432
+ // Reinitialize the viewer when leaving fullscreen to fix sizing issues
2433
+ if (wasFullscreen && !doc.fullscreenElement) {
2434
+ this.model3dTabTimeouts.later(() => void this.model3dTabInitViewer(), 100);
2435
+ }
2436
+ };
2437
+ doc.addEventListener('fullscreenchange', onFullscreenChange);
2438
+ inject(DestroyRef).onDestroy(() => {
2439
+ doc.removeEventListener('fullscreenchange', onFullscreenChange);
2440
+ });
2441
+ }
2442
+ }
2443
+ /** With `loadableOnly` (default) only models with a file to load. */
2444
+ mediaTabFilterModels(models) {
2445
+ if (this.model3dTabOptions.loadableOnly === false)
2446
+ return models;
2447
+ // Only models with a file to load — the same condition as the tab
2448
+ // counter, so the default model does not end in "No files found"
2449
+ // (MOB-1872 review).
2450
+ return models.filter((model) => (model.files ?? []).some((file) => !!file?.filePath));
2451
+ }
2452
+ /** Follows the `selectedModelIndex` dropdown: sets `selectedModel` and loads that model. */
2453
+ onModelChange() {
2454
+ this.selectedModel.set(Number(this.selectedModelIndex) || 0);
2455
+ this.model3dTabLoadModel(this.selectedModelIndex);
2456
+ }
2457
+ /** Fits the model to the window with the Y axis up. A no-op before the library is loaded. */
2458
+ resetCamera() {
2459
+ const library = this.model3dTabLibraryLoader?.loaded;
2460
+ if (this.model3dTabViewer && library) {
2461
+ // Reset the camera to fit the model and reset rotation
2462
+ const viewerInstance = this.model3dTabViewer.GetViewer();
2463
+ // Reset the up vector to default (Y-up)
2464
+ viewerInstance.SetUpVector(library.Direction.Y, false);
2465
+ // Get bounding sphere and fit to window
2466
+ const boundingSphere = viewerInstance.GetBoundingSphere(() => true);
2467
+ viewerInstance.AdjustClippingPlanesToSphere(boundingSphere);
2468
+ viewerInstance.FitSphereToWindow(boundingSphere, true); // Use animation for smooth transition
2469
+ }
2470
+ }
2471
+ /** Shows or hides the edges. A no-op before the library is loaded. */
2472
+ toggleWireframe() {
2473
+ const library = this.model3dTabLibraryLoader?.loaded;
2474
+ if (!this.model3dTabViewer || !library)
2475
+ return;
2476
+ const newWireframeState = !this.isWireframe();
2477
+ this.isWireframe.set(newWireframeState);
2478
+ const edgeSettings = new library.EdgeSettings(newWireframeState, new library.RGBColor(0, 0, 0), 1);
2479
+ const viewerInstance = this.model3dTabViewer.GetViewer();
2480
+ viewerInstance.SetEdgeSettings(edgeSettings);
2481
+ }
2482
+ /** Shows the parent of `#viewer3d` fullscreen, or leaves fullscreen. */
2483
+ toggleFullscreen() {
2484
+ const viewerElement = this.viewer3d();
2485
+ if (!viewerElement)
2486
+ return;
2487
+ const container = viewerElement.nativeElement.parentElement;
2488
+ const doc = this.model3dTabDocument;
2489
+ if (!doc.fullscreenElement) {
2490
+ container.requestFullscreen().catch((err) => {
2491
+ console.error('Error attempting to enable fullscreen:', err);
2492
+ });
2493
+ }
2494
+ else {
2495
+ doc.exitFullscreen();
2496
+ }
2497
+ }
2498
+ model3dTabLibrary() {
2499
+ this.model3dTabLibraryLoader ??= createLazyLoader(() => this.loadModel3dLibrary());
2500
+ return this.model3dTabLibraryLoader;
2501
+ }
2502
+ async model3dTabInitViewer() {
2503
+ if (!isPlatformBrowser(this.model3dTabPlatformId))
2504
+ return;
2505
+ const viewerElement = this.viewer3d();
2506
+ if (!viewerElement)
2507
+ return;
2508
+ const models = this.models();
2509
+ if (models.length === 0)
2510
+ return;
2511
+ const token = ++this.model3dTabInitToken;
2512
+ this.isLoading.set(true);
2513
+ this.error.set(null);
2514
+ let library;
2515
+ try {
2516
+ library = await this.model3dTabLibrary().load();
2517
+ }
2518
+ catch (loadError) {
2519
+ // The library chunk did not arrive (network, old deploy) — a message
2520
+ // instead of an endless loader; the next entry to the tab retries.
2521
+ if (token === this.model3dTabInitToken) {
2522
+ this.isLoading.set(false);
2523
+ this.error.set('3D viewer failed to load');
2524
+ }
2525
+ console.error('online-3d-viewer', loadError);
2526
+ return;
2527
+ }
2528
+ // A newer call came in meanwhile, or the component is gone.
2529
+ if (token !== this.model3dTabInitToken || !this.viewer3d())
2530
+ return;
2531
+ // Clean up existing viewer
2532
+ if (this.model3dTabViewer) {
2533
+ this.model3dTabViewer.Destroy();
2534
+ this.model3dTabViewer = null;
2535
+ }
2536
+ const parentDiv = viewerElement.nativeElement;
2537
+ parentDiv.innerHTML = ''; // Clear previous content
2538
+ // Initialize the viewer with callback for proper loading state
2539
+ this.model3dTabViewer = new library.EmbeddedViewer(parentDiv, {
2540
+ backgroundColor: new library.RGBAColor(245, 245, 245, 255),
2541
+ defaultColor: new library.RGBColor(200, 200, 200),
2542
+ edgeSettings: new library.EdgeSettings(false, new library.RGBColor(0, 0, 0), 1),
2543
+ onModelLoaded: () => {
2544
+ this.isLoading.set(false);
2545
+ },
2546
+ });
2547
+ this.model3dTabLoadModel(this.selectedModelIndex);
2548
+ }
2549
+ model3dTabLoadModel(index) {
2550
+ if (!this.model3dTabViewer)
2551
+ return;
2552
+ const models = this.models();
2553
+ if (index < 0 || index >= models.length)
2554
+ return;
2555
+ const model = models[index];
2556
+ this.isLoading.set(true);
2557
+ this.error.set(null);
2558
+ // Only files with a URL — an empty entry in `files[]` broke the 3D viewer
2559
+ // and blocked the model's valid files (MOB-1872 review).
2560
+ const fileUrls = (model.files ?? [])
2561
+ .map((file) => file?.filePath)
2562
+ .filter((url) => !!url);
2563
+ if (fileUrls.length === 0) {
2564
+ this.error.set('No files found for this model');
2565
+ this.isLoading.set(false);
2566
+ return;
2567
+ }
2568
+ // Load the model - callbacks onModelLoaded/onModelLoadFailed handle loading state
2569
+ this.model3dTabViewer.LoadModelFromUrlList(fileUrls);
2570
+ }
2571
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectModel3dTabBaseComponent, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive }); }
2572
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.3", type: ObjectModel3dTabBaseComponent, isStandalone: true, viewQueries: [{ propertyName: "viewer3d", first: true, predicate: ["viewer3d"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0 }); }
2573
+ }
2574
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectModel3dTabBaseComponent, decorators: [{
2575
+ type: Directive
2576
+ }], ctorParameters: () => [{ type: undefined }], propDecorators: { viewer3d: [{ type: i0.ViewChild, args: ['viewer3d', { isSignal: true }] }] } });
2577
+
2578
+ /**
2579
+ * Logic-only base of the sztuki-2 / template `media-sections`: an object's
2580
+ * multimedia as landing-page sections (Figma), each with a heading like
2581
+ * „Galeria zdjęć”; a section without data is not rendered. Order: HD photos →
2582
+ * 3D model → animation → video → audio → virtual walk → audio description →
2583
+ * map → other files. The component keeps its template, heading class and
2584
+ * tab imports.
2585
+ *
2586
+ * ```ts
2587
+ * export class MediaSectionsComponent extends ObjectMediaSectionsBaseComponent {
2588
+ * readonly sectionHeadingClass = '…';
2589
+ * }
2590
+ * ```
2591
+ */
2592
+ class ObjectMediaSectionsBaseComponent {
2593
+ constructor() {
2594
+ this.mediaSectionsFileUrl = inject(FileUrlService);
2595
+ this.mediaSectionsMedia = inject(ObjectMediaService);
2596
+ /** The object whose multimedia the sections show. */
2597
+ this.item = input.required(/* @ts-ignore */
2598
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
2599
+ /**
2600
+ * Multimedia already shown in the object card's gallery frame (the video /
2601
+ * 3D tabs, MOB-1961) — not repeated in the sections. Default `[]`.
2602
+ */
2603
+ this.exclude = input([], /* @ts-ignore */
2604
+ ...(ngDevMode ? [{ debugName: "exclude" }] : /* istanbul ignore next */ []));
2605
+ /** `[IMAGES_HD]` */
2606
+ this.imagesHDTypes = [FormFeatureType.IMAGES_HD];
2607
+ /** `[MODEL_3D]` */
2608
+ this.model3DTypes = [FormFeatureType.MODEL_3D];
2609
+ /** `[ANIMATION]` */
2610
+ this.animationTypes = [FormFeatureType.ANIMATION];
2611
+ /** `[AUDIO_DESKRYPCJA]` */
2612
+ this.adTypes = [FormFeatureType.AUDIO_DESKRYPCJA];
2613
+ /** `[VIRTUAL_WALK]` */
2614
+ this.virtualWalkTypes = [FormFeatureType.VIRTUAL_WALK];
2615
+ /** `[VIDEO]` */
2616
+ this.videoTypes = [FormFeatureType.VIDEO];
2617
+ /** `[AUDIO]` */
2618
+ this.audioTypes = [FormFeatureType.AUDIO];
2619
+ /** `[OTHER]` */
2620
+ this.otherTypes = [FormFeatureType.OTHER];
2621
+ /** HD photo thumbnails (without the main image) — fullscreen on click. */
2622
+ this.hdThumbnailUrls = computed(() => this.mediaSectionsMedia.thumbnailUrls(this.item(), this.imagesHDTypes, false), /* @ts-ignore */
2623
+ ...(ngDevMode ? [{ debugName: "hdThumbnailUrls" }] : /* istanbul ignore next */ []));
2624
+ /** HD photo gallery items (without the main image) for the fullscreen. */
2625
+ this.hdGalleryItems = computed(() => this.mediaSectionsMedia.galleryItems(this.item(), this.imagesHDTypes, false), /* @ts-ignore */
2626
+ ...(ngDevMode ? [{ debugName: "hdGalleryItems" }] : /* istanbul ignore next */ []));
2627
+ /** Whether the HD fullscreen gallery is open. */
2628
+ this.hdFullscreenOpen = signal(false, /* @ts-ignore */
2629
+ ...(ngDevMode ? [{ debugName: "hdFullscreenOpen" }] : /* istanbul ignore next */ []));
2630
+ /** The photo the HD fullscreen gallery shows. */
2631
+ this.hdFullscreenIndex = signal(0, /* @ts-ignore */
2632
+ ...(ngDevMode ? [{ debugName: "hdFullscreenIndex" }] : /* istanbul ignore next */ []));
2633
+ /** The object title without HTML tags (alt text). */
2634
+ this.altTitle = computed(() => (this.item()?.title ?? '').replace(/<\/?[^>]+(>|$)/g, ''), /* @ts-ignore */
2635
+ ...(ngDevMode ? [{ debugName: "altTitle" }] : /* istanbul ignore next */ []));
2636
+ /** HD photos section: any HD thumbnail. */
2637
+ this.showPhotosHD = computed(() => this.hdThumbnailUrls().length > 0, /* @ts-ignore */
2638
+ ...(ngDevMode ? [{ debugName: "showPhotosHD" }] : /* istanbul ignore next */ []));
2639
+ /** 3D model section: a real file URL, unless excluded. */
2640
+ this.showModel3D = computed(() => !this.exclude().includes('model3d') &&
2641
+ this.mediaSectionsHasContent(this.model3DTypes), /* @ts-ignore */
2642
+ ...(ngDevMode ? [{ debugName: "showModel3D" }] : /* istanbul ignore next */ []));
2643
+ /** Animation section: a real file URL. */
2644
+ this.showAnimation = computed(() => this.mediaSectionsHasContent(this.animationTypes), /* @ts-ignore */
2645
+ ...(ngDevMode ? [{ debugName: "showAnimation" }] : /* istanbul ignore next */ []));
2646
+ /**
2647
+ * Video section: a file with a `filePath` (`FileUrlService` gives a
2648
+ * placeholder for non-image extensions), unless excluded.
2649
+ */
2650
+ this.showVideo = computed(() => !this.exclude().includes('video') &&
2651
+ this.mediaSectionsMedia.hasFiles(this.item(), this.videoTypes), /* @ts-ignore */
2652
+ ...(ngDevMode ? [{ debugName: "showVideo" }] : /* istanbul ignore next */ []));
2653
+ /** Audio section: a file with a `filePath`. */
2654
+ this.showAudio = computed(() => this.mediaSectionsMedia.hasFiles(this.item(), this.audioTypes), /* @ts-ignore */
2655
+ ...(ngDevMode ? [{ debugName: "showAudio" }] : /* istanbul ignore next */ []));
2656
+ /** Virtual walk section: a walk with an `iframeSource` (not a file URL). */
2657
+ this.showVirtualWalk = computed(() => {
2658
+ const obj = this.item();
2659
+ if (!obj?.additionalImages)
2660
+ return false;
2661
+ const virtualWalkItems = obj.additionalImages.filter((img) => this.virtualWalkTypes.includes(img?.formFeature));
2662
+ // Any virtual walk item with an iframeSource
2663
+ return virtualWalkItems.some((item) => item.iframeSource);
2664
+ }, /* @ts-ignore */
2665
+ ...(ngDevMode ? [{ debugName: "showVirtualWalk" }] : /* istanbul ignore next */ []));
2666
+ /** The number of audio description files with a real URL. */
2667
+ this.audioDescriptionCount = computed(() => {
2668
+ const obj = this.item();
2669
+ if (!obj?.additionalImages)
2670
+ return 0;
2671
+ const audioItems = obj.additionalImages.filter((img) => {
2672
+ if (!this.adTypes.includes(img?.formFeature))
2673
+ return false;
2674
+ const url = this.mediaSectionsFileUrl.get(img);
2675
+ return url && !url.includes('assets/no-image.svg');
2676
+ });
2677
+ return audioItems.length;
2678
+ }, /* @ts-ignore */
2679
+ ...(ngDevMode ? [{ debugName: "audioDescriptionCount" }] : /* istanbul ignore next */ []));
2680
+ /** Audio description section: any audio description. */
2681
+ this.showAudioDescription = computed(() => this.audioDescriptionCount() > 0, /* @ts-ignore */
2682
+ ...(ngDevMode ? [{ debugName: "showAudioDescription" }] : /* istanbul ignore next */ []));
2683
+ /** Map section: both `archeoMap` coordinates set. */
2684
+ this.showMap = computed(() => {
2685
+ const obj = this.item();
2686
+ return !!(obj?.archeoMap?.latitude && obj?.archeoMap?.longitude);
2687
+ }, /* @ts-ignore */
2688
+ ...(ngDevMode ? [{ debugName: "showMap" }] : /* istanbul ignore next */ []));
2689
+ /** Other files section (PDF, etc.): a real file URL. */
2690
+ this.showOther = computed(() => this.mediaSectionsHasContent(this.otherTypes), /* @ts-ignore */
2691
+ ...(ngDevMode ? [{ debugName: "showOther" }] : /* istanbul ignore next */ []));
2692
+ /** Whether any section is visible. */
2693
+ this.hasVisibleSections = computed(() => this.showPhotosHD() ||
2694
+ this.showModel3D() ||
2695
+ this.showAnimation() ||
2696
+ this.showVideo() ||
2697
+ this.showAudio() ||
2698
+ this.showVirtualWalk() ||
2699
+ this.showAudioDescription() ||
2700
+ this.showMap() ||
2701
+ this.showOther(), /* @ts-ignore */
2702
+ ...(ngDevMode ? [{ debugName: "hasVisibleSections" }] : /* istanbul ignore next */ []));
2703
+ }
2704
+ /** Opens the HD fullscreen gallery at the clicked thumbnail. */
2705
+ openHdFullscreen(index) {
2706
+ this.hdFullscreenIndex.set(index);
2707
+ this.hdFullscreenOpen.set(true);
2708
+ }
2709
+ /**
2710
+ * Whether an additional file of the types has a URL from `FileUrlService`
2711
+ * that is not the no-image placeholder. Reads `item()`, so the calling
2712
+ * `computed` tracks it.
2713
+ */
2714
+ mediaSectionsHasContent(types) {
2715
+ const obj = this.item();
2716
+ if (!obj)
2717
+ return false;
2718
+ // Check additional images
2719
+ if (obj.additionalImages && Array.isArray(obj.additionalImages)) {
2720
+ const filtered = obj.additionalImages.filter((img) => types.includes(img?.formFeature));
2721
+ for (const img of filtered) {
2722
+ const url = this.mediaSectionsFileUrl.get(img);
2723
+ if (url && !url.includes('assets/no-image.svg')) {
2724
+ return true;
2725
+ }
2726
+ }
2727
+ }
2728
+ return false;
2729
+ }
2730
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectMediaSectionsBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2731
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: ObjectMediaSectionsBaseComponent, isStandalone: true, inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: true, transformFunction: null }, exclude: { classPropertyName: "exclude", publicName: "exclude", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
2732
+ }
2733
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: ObjectMediaSectionsBaseComponent, decorators: [{
2734
+ type: Directive
2735
+ }], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: true }] }], exclude: [{ type: i0.Input, args: [{ isSignal: true, alias: "exclude", required: false }] }] } });
2736
+
1744
2737
  /**
1745
2738
  * Generated bundle index. Do not edit.
1746
2739
  */
1747
2740
 
1748
- export { ASK_ABOUT_OBJECT_KEYS, AdvancedFilterType, ComponentsModule, FiltersComponent, FormFeatureType, ListComponent, MULTIMEDIA_DICTIONARY, OBJECTS_CATALOG_DEFAULTS, OBJECTS_CATALOG_SEO, OBJECTS_FILTER_SINGLE_VALUE_LABELS, OBJECTS_LIMIT_OPTIONS, OBJECTS_NEW_TYPE_SORT_OPTIONS, OBJECTS_SHARE_COPIED_TIMEOUT, OBJECTS_SORT_OPTIONS, OBJECTS_THEME_TAGS_FILTER_KEY, ObjectDetailsBaseComponent, ObjectMediaService, ObjectsAdvancedFiltersBaseComponent, ObjectsCatalogBaseComponent, ObjectsDataService, ObjectsFacade, ObjectsFiltersBarBaseComponent, ObjectsItemComponent, ObjectsListComponent, ObjectsModule, ObjectsSearchPanelBaseComponent, ObjectsService, ObjectsShareResultsBaseComponent, ObjectsSharedModule, SERVICES, askAboutObjectHref, bibliography, buildObjectsResultsUrl, descriptionHtml, getObjectsState, getReducer, initialState, inventoryValues, objectTitle, objectsSortFromKey, objectsSortKey, relatedArticles, tagValues };
2741
+ export { ASK_ABOUT_OBJECT_KEYS, AdvancedFilterType, ComponentsModule, FiltersComponent, FormFeatureType, ListComponent, MULTIMEDIA_DICTIONARY, OBJECTS_CATALOG_DEFAULTS, OBJECTS_CATALOG_SEO, OBJECTS_FILTER_SINGLE_VALUE_LABELS, OBJECTS_LIMIT_OPTIONS, OBJECTS_NEW_TYPE_SORT_OPTIONS, OBJECTS_SHARE_COPIED_TIMEOUT, OBJECTS_SORT_OPTIONS, OBJECTS_THEME_TAGS_FILTER_KEY, ObjectAnimationTabBaseComponent, ObjectAudioDescriptionTabBaseComponent, ObjectAudioMediaTabBaseComponent, ObjectAudioTabBaseComponent, ObjectDetailsBaseComponent, ObjectDocumentsTabBaseComponent, ObjectDownloadButtonBaseComponent, ObjectMapTabBaseComponent, ObjectMediaSectionsBaseComponent, ObjectMediaService, ObjectMediaTabBaseComponent, ObjectModel3dTabBaseComponent, ObjectVideoMediaTabBaseComponent, ObjectVideoTabBaseComponent, ObjectVirtualWalkTabBaseComponent, ObjectsAdvancedFiltersBaseComponent, ObjectsCatalogBaseComponent, ObjectsDataService, ObjectsFacade, ObjectsFiltersBarBaseComponent, ObjectsItemComponent, ObjectsListComponent, ObjectsModule, ObjectsSearchPanelBaseComponent, ObjectsService, ObjectsShareResultsBaseComponent, ObjectsSharedModule, SERVICES, askAboutObjectHref, bibliography, buildObjectsResultsUrl, descriptionHtml, getObjectsState, getReducer, initialState, inventoryValues, objectTitle, objectsSortFromKey, objectsSortKey, relatedArticles, tagValues };
1749
2742
  //# sourceMappingURL=smartsoft001-mobilems-objects-shell-angular.mjs.map