Proyecto App Windows
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'portal-catalog-feature',
|
||||
preset: '../../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../../coverage/libs/portal/catalog/feature',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "portal-catalog-feature",
|
||||
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/portal/catalog/feature/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:portal", "domain:portal-shared", "type:feature"],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/portal/catalog/feature/jest.config.ts",
|
||||
"tsConfig": "libs/portal/catalog/feature/tsconfig.spec.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './lib/category-content-view/category-content-view.component';
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
@if (selectedCategory()) {
|
||||
@if (selectedDetailComponent(); as detailComponentRef) {
|
||||
<ng-container *ngComponentOutlet="detailComponentRef" />
|
||||
} @else {
|
||||
<div class="category-content-layout">
|
||||
<div class="category-content-header">
|
||||
<div class="category-meta">
|
||||
<h2 class="category-title">
|
||||
{{ selectedCategoryTitle() }}
|
||||
</h2>
|
||||
<div class="category-subtitle">
|
||||
{{ categoryItemSubtitle() }}
|
||||
</div>
|
||||
</div>
|
||||
@if (canSortContent()) {
|
||||
<button
|
||||
type="button"
|
||||
mat-icon-button
|
||||
class="sort-action"
|
||||
[matMenuTriggerFor]="sortContentMenu"
|
||||
matTooltip="Sort content"
|
||||
aria-label="Sort content"
|
||||
>
|
||||
<mat-icon>sort</mat-icon>
|
||||
</button>
|
||||
<mat-menu #sortContentMenu="matMenu">
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setContentSortMode('date-desc')"
|
||||
>
|
||||
<span>{{
|
||||
'WORKSPACE.SORT_DATE_DESC' | translate
|
||||
}}</span>
|
||||
@if (contentSortMode() === 'date-desc') {
|
||||
<mat-icon>check</mat-icon>
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setContentSortMode('date-asc')"
|
||||
>
|
||||
<span>{{
|
||||
'WORKSPACE.SORT_DATE_ASC' | translate
|
||||
}}</span>
|
||||
@if (contentSortMode() === 'date-asc') {
|
||||
<mat-icon>check</mat-icon>
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setContentSortMode('name-asc')"
|
||||
>
|
||||
<span>{{
|
||||
'WORKSPACE.SORT_NAME_ASC' | translate
|
||||
}}</span>
|
||||
@if (contentSortMode() === 'name-asc') {
|
||||
<mat-icon>check</mat-icon>
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setContentSortMode('name-desc')"
|
||||
>
|
||||
<span>{{
|
||||
'WORKSPACE.SORT_NAME_DESC' | translate
|
||||
}}</span>
|
||||
@if (contentSortMode() === 'name-desc') {
|
||||
<mat-icon>check</mat-icon>
|
||||
}
|
||||
</button>
|
||||
</mat-menu>
|
||||
}
|
||||
@if (categoryItemCount() > 0) {
|
||||
<mat-paginator
|
||||
[pageIndex]="pageIndex()"
|
||||
[length]="categoryItemCount()"
|
||||
[pageSize]="limit()"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)"
|
||||
aria-label="Select page"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<app-grid-list
|
||||
[isLoading]="isPaginatedContentLoading()"
|
||||
[items]="contentWithProgress()"
|
||||
(itemClicked)="onItemClick($event)"
|
||||
[pageIndex]="pageIndex()"
|
||||
[totalPages]="totalPages()"
|
||||
[limit]="limit()"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
[showPaginator]="false"
|
||||
(pageChange)="onPageChange($event)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<app-playlist-error-view
|
||||
[title]="'PORTALS.ERROR_VIEW.NO_CATEGORY_SELECTED.TITLE' | translate"
|
||||
[description]="
|
||||
'PORTALS.ERROR_VIEW.NO_CATEGORY_SELECTED.DESCRIPTION' | translate
|
||||
"
|
||||
[showActionButtons]="false"
|
||||
[viewType]="'EMPTY_CATEGORY'"
|
||||
/>
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
@use '../../../../../../ui/styles/panel-header' as panel;
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-content-layout {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ─── Sticky header bar — styled like a widget header ─────────────────────────
|
||||
.category-content-header {
|
||||
@include panel.standard-panel-header($sticky: true);
|
||||
}
|
||||
|
||||
// ─── Category meta (title + subtitle) ────────────────────────────────────────
|
||||
.category-meta {
|
||||
@include panel.standard-panel-meta();
|
||||
}
|
||||
|
||||
.category-title {
|
||||
@include panel.standard-panel-title();
|
||||
}
|
||||
|
||||
.category-subtitle {
|
||||
@include panel.standard-panel-subtitle();
|
||||
}
|
||||
|
||||
// ─── Paginator — flush right, no ghost background ────────────────────────────
|
||||
mat-paginator {
|
||||
background: transparent !important;
|
||||
flex-shrink: 0;
|
||||
// Shrink paginator text to fit header height comfortably
|
||||
--mat-paginator-container-size: 40px;
|
||||
--mat-paginator-enabled-icon-color: var(--app-body-color, var(--mat-sys-on-surface-variant));
|
||||
--mat-paginator-disabled-icon-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
// ─── Grid scroll area ─────────────────────────────────────────────────────────
|
||||
app-grid-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px 20px;
|
||||
display: block;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.08) transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.category-content-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 10px 16px;
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
.category-meta {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
app-grid-list {
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
}
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
import { Component, input, output, signal } from '@angular/core';
|
||||
import { NgComponentOutlet } from '@angular/common';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { ReplaySubject, of } from 'rxjs';
|
||||
import {
|
||||
PORTAL_CATALOG_DETAIL_COMPONENT,
|
||||
PORTAL_CATALOG_FACADE,
|
||||
PortalCatalogSortMode,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { CategoryContentViewComponent } from './category-content-view.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-grid-list',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class MockGridListComponent {
|
||||
readonly isLoading = input<boolean>();
|
||||
readonly items = input<unknown[]>();
|
||||
readonly pageIndex = input<number>();
|
||||
readonly totalPages = input<number>();
|
||||
readonly limit = input<number>();
|
||||
readonly pageSizeOptions = input<number[]>();
|
||||
readonly showPaginator = input(true);
|
||||
readonly itemClicked = output<unknown>();
|
||||
readonly pageChange = output<unknown>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-error-view',
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class MockPlaylistErrorViewComponent {
|
||||
readonly title = input('');
|
||||
readonly description = input('');
|
||||
readonly showActionButtons = input(true);
|
||||
readonly viewType = input('');
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: '',
|
||||
})
|
||||
class MockDetailComponent {}
|
||||
|
||||
describe('CategoryContentViewComponent', () => {
|
||||
let fixture: ComponentFixture<CategoryContentViewComponent>;
|
||||
let router: { navigate: jest.Mock };
|
||||
const paramMap$ = new ReplaySubject(1);
|
||||
const queryParamMap$ = new ReplaySubject(1);
|
||||
const isPaginatedContentLoading = signal(true);
|
||||
const categoryItemCount = signal(0);
|
||||
const contentSortMode = signal<PortalCatalogSortMode | null>(null);
|
||||
const catalog = {
|
||||
provider: 'xtream' as const,
|
||||
pageSizeOptions: [10, 25, 50],
|
||||
contentType: signal('vod'),
|
||||
limit: signal(25),
|
||||
pageIndex: signal(0),
|
||||
selectedCategory: signal({ id: 1 }),
|
||||
paginatedContent: signal<unknown[]>([]),
|
||||
selectedCategoryTitle: signal('Movies'),
|
||||
categoryItemCount,
|
||||
selectedItem: signal(null),
|
||||
totalPages: signal(0),
|
||||
contentSortMode,
|
||||
playlist: signal(null),
|
||||
isPaginatedContentLoading,
|
||||
initialize: jest.fn(),
|
||||
setSearchQuery: jest.fn(),
|
||||
clearSelectedItem: jest.fn(),
|
||||
setPage: jest.fn(),
|
||||
setLimit: jest.fn(),
|
||||
setContentSortMode: jest.fn(),
|
||||
selectItem: jest.fn().mockReturnValue(null),
|
||||
getItemProgress: jest.fn().mockReturnValue({}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
isPaginatedContentLoading.set(true);
|
||||
categoryItemCount.set(0);
|
||||
contentSortMode.set(null);
|
||||
catalog.initialize.mockClear();
|
||||
catalog.setSearchQuery.mockClear();
|
||||
catalog.setPage.mockClear();
|
||||
catalog.setLimit.mockClear();
|
||||
catalog.selectItem.mockClear();
|
||||
catalog.selectItem.mockReturnValue(null);
|
||||
router = {
|
||||
navigate: jest.fn(),
|
||||
};
|
||||
paramMap$.next(convertToParamMap({}));
|
||||
queryParamMap$.next(convertToParamMap({}));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CategoryContentViewComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: {
|
||||
instant: (key: string) =>
|
||||
key === 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING'
|
||||
? 'Fetching playlist data from source...'
|
||||
: key,
|
||||
get: (key: string) =>
|
||||
of(
|
||||
key === 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING'
|
||||
? 'Fetching playlist data from source...'
|
||||
: key
|
||||
),
|
||||
stream: (key: string) =>
|
||||
of(
|
||||
key === 'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING'
|
||||
? 'Fetching playlist data from source...'
|
||||
: key
|
||||
),
|
||||
onLangChange: of(null),
|
||||
onTranslationChange: of(null),
|
||||
onDefaultLangChange: of(null),
|
||||
currentLang: 'en',
|
||||
defaultLang: 'en',
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PORTAL_CATALOG_FACADE,
|
||||
useValue: catalog,
|
||||
},
|
||||
{
|
||||
provide: PORTAL_CATALOG_DETAIL_COMPONENT,
|
||||
useValue: MockDetailComponent,
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
paramMap: paramMap$.asObservable(),
|
||||
queryParamMap: queryParamMap$.asObservable(),
|
||||
snapshot: {
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Router,
|
||||
useValue: router,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(CategoryContentViewComponent, {
|
||||
set: {
|
||||
imports: [
|
||||
NgComponentOutlet,
|
||||
MockGridListComponent,
|
||||
MockPlaylistErrorViewComponent,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltip,
|
||||
TranslatePipe,
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CategoryContentViewComponent);
|
||||
});
|
||||
|
||||
it('shows loading copy in the subtitle instead of 0 items while xtream content is still warming up', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
const subtitle = fixture.nativeElement.querySelector(
|
||||
'.category-subtitle'
|
||||
) as HTMLElement | null;
|
||||
|
||||
expect(catalog.initialize).toHaveBeenCalledWith(null);
|
||||
expect(subtitle?.textContent?.trim()).toBe(
|
||||
'Fetching playlist data from source...'
|
||||
);
|
||||
});
|
||||
|
||||
it('forwards query-param search updates to the catalog facade when supported', () => {
|
||||
fixture.detectChanges();
|
||||
catalog.setSearchQuery.mockClear();
|
||||
|
||||
queryParamMap$.next(
|
||||
convertToParamMap({
|
||||
q: 'matrix',
|
||||
})
|
||||
);
|
||||
|
||||
expect(catalog.setSearchQuery).toHaveBeenCalledWith('matrix');
|
||||
});
|
||||
|
||||
it('restores the zero-based catalog page from the one-based page query param', () => {
|
||||
fixture.detectChanges();
|
||||
catalog.setPage.mockClear();
|
||||
|
||||
queryParamMap$.next(
|
||||
convertToParamMap({
|
||||
page: '3',
|
||||
})
|
||||
);
|
||||
|
||||
expect(catalog.setPage).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('falls back to the first catalog page when the page query param is absent or invalid', () => {
|
||||
fixture.detectChanges();
|
||||
catalog.setPage.mockClear();
|
||||
|
||||
queryParamMap$.next(convertToParamMap({}));
|
||||
queryParamMap$.next(
|
||||
convertToParamMap({
|
||||
page: 'not-a-page',
|
||||
})
|
||||
);
|
||||
|
||||
expect(catalog.setPage).toHaveBeenNthCalledWith(1, 0);
|
||||
expect(catalog.setPage).toHaveBeenNthCalledWith(2, 0);
|
||||
});
|
||||
|
||||
it('writes one-based page query params when the paginator changes', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.onPageChange({
|
||||
length: 100,
|
||||
pageIndex: 1,
|
||||
pageSize: 25,
|
||||
previousPageIndex: 0,
|
||||
});
|
||||
|
||||
expect(catalog.setPage).toHaveBeenCalledWith(1);
|
||||
expect(catalog.setLimit).toHaveBeenCalledWith(25);
|
||||
expect(router.navigate).toHaveBeenCalledWith([], {
|
||||
relativeTo: expect.any(Object),
|
||||
queryParams: {
|
||||
page: 2,
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('removes the page query param when returning to the first page', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.onPageChange({
|
||||
length: 100,
|
||||
pageIndex: 0,
|
||||
pageSize: 25,
|
||||
previousPageIndex: 1,
|
||||
});
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith([], {
|
||||
relativeTo: expect.any(Object),
|
||||
queryParams: {
|
||||
page: null,
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('scrolls the grid list host to the top when the paginator changes', () => {
|
||||
fixture.detectChanges();
|
||||
const gridList = fixture.nativeElement.querySelector(
|
||||
'app-grid-list'
|
||||
) as HTMLElement;
|
||||
const scrollTo = jest.fn();
|
||||
Object.defineProperty(gridList, 'scrollTo', {
|
||||
configurable: true,
|
||||
value: scrollTo,
|
||||
});
|
||||
|
||||
fixture.componentInstance.onPageChange({
|
||||
length: 100,
|
||||
pageIndex: 1,
|
||||
pageSize: 25,
|
||||
previousPageIndex: 0,
|
||||
});
|
||||
|
||||
expect(scrollTo).toHaveBeenCalledWith({ top: 0 });
|
||||
});
|
||||
|
||||
it('preserves query params when navigating from an item to Xtream details', () => {
|
||||
catalog.selectItem.mockReturnValue(['42']);
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.componentInstance.onItemClick({
|
||||
xtream_id: 42,
|
||||
});
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['42'], {
|
||||
relativeTo: expect.any(Object),
|
||||
queryParamsHandling: 'preserve',
|
||||
});
|
||||
});
|
||||
});
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import { NgComponentOutlet } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
computed,
|
||||
DestroyRef,
|
||||
ElementRef,
|
||||
inject,
|
||||
OnInit,
|
||||
} from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
GridListComponent,
|
||||
PlaylistErrorViewComponent,
|
||||
} from '@iptvnator/portal/shared/ui';
|
||||
import {
|
||||
clearNavigationStateKeys,
|
||||
getOpenStalkerItemState,
|
||||
PortalCatalogFacade,
|
||||
OPEN_STALKER_ITEM_STATE_KEY,
|
||||
PORTAL_CATALOG_DETAIL_COMPONENT,
|
||||
PORTAL_CATALOG_FACADE,
|
||||
PortalCatalogSortMode,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
|
||||
interface CategoryContentItem {
|
||||
id?: number | string;
|
||||
is_series?: number | string | boolean;
|
||||
xtream_id?: number | string;
|
||||
series_id?: number | string;
|
||||
stream_id?: number | string;
|
||||
category_id?: number | string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-category-content-view',
|
||||
templateUrl: './category-content-view.component.html',
|
||||
styleUrls: ['./category-content-view.component.scss'],
|
||||
imports: [
|
||||
GridListComponent,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltip,
|
||||
NgComponentOutlet,
|
||||
PlaylistErrorViewComponent,
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
export class CategoryContentViewComponent implements OnInit {
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly hostElement = inject(ElementRef<HTMLElement>);
|
||||
private readonly router = inject(Router);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly catalog = inject(
|
||||
PORTAL_CATALOG_FACADE
|
||||
) as PortalCatalogFacade<
|
||||
CategoryContentItem,
|
||||
CategoryContentItem,
|
||||
CategoryContentItem
|
||||
>;
|
||||
|
||||
readonly detailComponent = inject(PORTAL_CATALOG_DETAIL_COMPONENT);
|
||||
readonly contentType = this.catalog.contentType;
|
||||
readonly limit = this.catalog.limit;
|
||||
readonly pageIndex = this.catalog.pageIndex;
|
||||
readonly pageSizeOptions = Array.from(this.catalog.pageSizeOptions);
|
||||
readonly selectedCategory = this.catalog.selectedCategory;
|
||||
readonly paginatedContent = this.catalog.paginatedContent;
|
||||
readonly selectedCategoryTitle = this.catalog.selectedCategoryTitle;
|
||||
readonly categoryItemCount = this.catalog.categoryItemCount;
|
||||
readonly selectedItem = this.catalog.selectedItem;
|
||||
readonly totalPages = this.catalog.totalPages;
|
||||
readonly contentSortMode = this.catalog.contentSortMode;
|
||||
readonly isPaginatedContentLoading = this.catalog.isPaginatedContentLoading;
|
||||
readonly isXtreamLoadingSubtitle = computed(
|
||||
() =>
|
||||
this.catalog.provider === 'xtream' &&
|
||||
this.isPaginatedContentLoading()
|
||||
);
|
||||
readonly categoryItemSubtitle = computed(() => {
|
||||
if (this.isXtreamLoadingSubtitle()) {
|
||||
return this.translate.instant(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING'
|
||||
);
|
||||
}
|
||||
|
||||
const itemCount = this.categoryItemCount();
|
||||
return `${itemCount} ${itemCount === 1 ? 'item' : 'items'}`;
|
||||
});
|
||||
readonly canSortContent = computed(() => this.contentSortMode() !== null);
|
||||
readonly selectedDetailComponent = computed(() =>
|
||||
this.selectedItem() ? this.detailComponent : null
|
||||
);
|
||||
readonly contentWithProgress = computed(() =>
|
||||
(this.paginatedContent() ?? []).map((item: CategoryContentItem) => ({
|
||||
...item,
|
||||
...this.catalog.getItemProgress(item),
|
||||
}))
|
||||
);
|
||||
|
||||
setContentSortMode(mode: PortalCatalogSortMode): void {
|
||||
this.catalog.setContentSortMode(mode);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.activatedRoute.paramMap
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((params) => {
|
||||
this.catalog.initialize(params.get('categoryId'));
|
||||
this.openStalkerItemFromNavigationState();
|
||||
});
|
||||
|
||||
this.activatedRoute.queryParamMap
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((params) => {
|
||||
this.catalog.setSearchQuery?.(params.get('q') ?? '');
|
||||
this.catalog.setPage(this.toPageIndex(params.get('page')));
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
this.catalog.setPage(event.pageIndex);
|
||||
this.catalog.setLimit(event.pageSize);
|
||||
this.scrollGridToTop();
|
||||
|
||||
void this.router.navigate([], {
|
||||
relativeTo: this.activatedRoute,
|
||||
queryParams: {
|
||||
page: event.pageIndex > 0 ? event.pageIndex + 1 : null,
|
||||
},
|
||||
queryParamsHandling: 'merge',
|
||||
replaceUrl: true,
|
||||
});
|
||||
}
|
||||
|
||||
onItemClick(item: CategoryContentItem): void {
|
||||
const navigation = this.catalog.selectItem(item);
|
||||
if (navigation?.length) {
|
||||
this.router.navigate(navigation, {
|
||||
relativeTo: this.activatedRoute,
|
||||
queryParamsHandling: 'preserve',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private toPageIndex(value: string | null): number {
|
||||
const page = Number(value);
|
||||
return Number.isInteger(page) && page > 0 ? page - 1 : 0;
|
||||
}
|
||||
|
||||
private scrollGridToTop(): void {
|
||||
const gridList = this.hostElement.nativeElement.querySelector(
|
||||
'app-grid-list'
|
||||
) as HTMLElement | null;
|
||||
gridList?.scrollTo?.({ top: 0 });
|
||||
}
|
||||
|
||||
private openStalkerItemFromNavigationState(): void {
|
||||
if (this.catalog.provider !== 'stalker') {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = getOpenStalkerItemState(window.history.state);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.catalog.selectItem(item as CategoryContentItem);
|
||||
clearNavigationStateKeys([
|
||||
OPEN_STALKER_ITEM_STATE_KEY,
|
||||
'openFavoriteItem',
|
||||
'openRecentItem',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user