chore(deps): upgrade Angular to 22.1 and Nx to 23.2 (#1603)

* chore(deps): upgrade Angular to 22.1 and Nx to 23.2

* fix(deps): complete Angular migrations after rebasing on master

* fix(ci): use the Node pin for Windows runtime refresh

* docs(deps): synchronize the workspace-shell Node requirements
This commit is contained in:
4gray authored and GitHub committed 2026-09-14 19:02:40 +02:00
1 parent ef3f98d026
commit e9eca1c386
200 files changed
+3445 -6820

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
type: internal
area: deps
---
The application uses Angular 22 and Nx 23 with updated build and test tooling.
+2 -2
View File
@@ -61,7 +61,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
node-version-file: '.nvmrc'
- name: Resolve Linux runtime toolchain cache key
id: linux-runtime-cache-key
@@ -379,7 +379,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install Linux system dependencies
+2 -2
View File
@@ -103,7 +103,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
@@ -141,7 +141,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
+2 -2
View File
@@ -59,7 +59,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install Dependencies
@@ -113,7 +113,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install Dependencies
@@ -35,7 +35,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: '22'
node-version-file: '.nvmrc'
- name: Refresh pin when it approaches upstream retention
id: refresh
+1
View File
@@ -0,0 +1 @@
22.23.2
+11 -4
View File
@@ -19,10 +19,12 @@ This file provides guidance to coding agents working in this repository.
- See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy.
- Keep `nx` and every official `@nx/*` package on the same exact version; run
`pnpm run deps:nx:validate` after dependency updates.
- Vite `7.3.6`, resolved through Angular's build tooling, is patched with
bounded transform prefilters and the upstream precise matchers in
`patches/vite@7.3.6.patch`. Keep the patch until supported Angular tooling
resolves a Vite version containing the fix, and run `pnpm run deps:vite:test`
- Use the Node version in `.nvmrc` for development and CI. Angular 22 requires
Node `^22.22.3 || ^24.15.0` and TypeScript `>=6.0 <6.1` in this workspace.
- Vite `8.1.5`, resolved through Angular's build tooling, retains upstream
precise matchers and adds bounded raw-code prefilters through
`patches/vite@8.1.5.patch`. Keep the patch until upstream also preserves
comment-bearing asset/worker expressions; run `pnpm run deps:vite:test`
after related dependency updates.
- `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in
`patches/app-builder-lib@26.15.7.patch` with the upstream backport
@@ -35,6 +37,11 @@ name or passphrase you entered is not correct`. Keep the patch until
and run `pnpm run deps:electron-builder:test` after related dependency
updates — the test fails when the patched version no longer matches the
installed one.
- `nx-electron@22.0.0` uses a local Nx 23 export-path patch and an explicit
`webpack-node-externals` package extension. Scoped peer allowances for it
and `ngx-indexed-db@22.0.0` live in `pnpm-workspace.yaml`; they are project
compatibility bridges, not upstream support declarations. See
`docs/architecture/nx-workspace-boundaries.md` before removing them.
- A directory holding files consumed by other projects must be an Nx project.
Nx builds its graph from TypeScript imports only, so a relative SCSS `@use`
across project roots creates no edge and the imported file lands in no task
+11 -4
View File
@@ -97,10 +97,12 @@ pnpm nx show projects
- See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy.
- Keep `nx` and every official `@nx/*` package on the same exact version; run
`pnpm run deps:nx:validate` after dependency updates.
- Vite `7.3.6`, resolved through Angular's build tooling, is patched with
bounded transform prefilters and the upstream precise matchers in
`patches/vite@7.3.6.patch`. Keep the patch until supported Angular tooling
resolves a Vite version containing the fix, and run `pnpm run deps:vite:test`
- Use the Node version in `.nvmrc` for development and CI. Angular 22 requires
Node `^22.22.3 || ^24.15.0` and TypeScript `>=6.0 <6.1` in this workspace.
- Vite `8.1.5`, resolved through Angular's build tooling, retains upstream
precise matchers and adds bounded raw-code prefilters through
`patches/vite@8.1.5.patch`. Keep the patch until upstream also preserves
comment-bearing asset/worker expressions; run `pnpm run deps:vite:test`
after related dependency updates.
- `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in
`patches/app-builder-lib@26.15.7.patch` with the upstream backport
@@ -113,6 +115,11 @@ name or passphrase you entered is not correct`. Keep the patch until
and run `pnpm run deps:electron-builder:test` after related dependency
updates — the test fails when the patched version no longer matches the
installed one.
- `nx-electron@22.0.0` uses a local Nx 23 export-path patch and an explicit
`webpack-node-externals` package extension. Scoped peer allowances for it
and `ngx-indexed-db@22.0.0` live in `pnpm-workspace.yaml`; they are project
compatibility bridges, not upstream support declarations. See
`docs/architecture/nx-workspace-boundaries.md` before removing them.
- A directory holding files consumed by other projects must be an Nx project.
Nx builds its graph from TypeScript imports only, so a relative SCSS `@use`
across project roots creates no edge and the imported file lands in no task
+8 -1
View File
@@ -272,11 +272,18 @@ needed.
Requirements:
- Node.js 22.13–22.x or 24 and newer with pnpm (via Corepack)
- Node.js 22.22.3 or newer within 22.x, or 24.15.0 or newer within 24.x
- pnpm 10.33.0 (via Corepack)
The repository's `.nvmrc` pins the Node.js version used by CI. With nvm,
run `nvm install` and `nvm use` from the repository root to use the same version
for local development.
1. Clone this repository and install project dependencies:
```
$ nvm install
$ nvm use
$ corepack enable
$ pnpm install
```
+2 -1
View File
@@ -9,7 +9,8 @@
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"ignoreDeprecations": "6.0"
},
"include": [
"**/*.ts",
+2 -1
View File
@@ -3,7 +3,8 @@
"compilerOptions": {
"types": ["node"],
"moduleResolution": "nodenext",
"module": "NodeNext"
"module": "NodeNext",
"ignoreDeprecations": "6.0"
},
"include": ["**/*.ts"]
}
@@ -4,7 +4,7 @@ import {
provideBrowserGlobalErrorListeners,
provideZoneChangeDetection,
} from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClient, withXhr } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';
@@ -17,7 +17,7 @@ export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(),
provideHttpClient(withXhr()),
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'en',
+8 -1
View File
@@ -1,4 +1,9 @@
import { Component, OnInit, inject } from '@angular/core';
import {
Component,
OnInit,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { RemoteControlComponent } from '@iptvnator/ui/remote-control';
import { TranslateService } from '@ngx-translate/core';
@@ -6,6 +11,8 @@ import { TranslateService } from '@ngx-translate/core';
imports: [RemoteControlComponent],
selector: 'app-root',
templateUrl: './app.html',
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './app.scss',
})
export class App implements OnInit {
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+9 -1
View File
@@ -10,5 +10,13 @@
"src/test-setup.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts"
]
],
"angularCompilerOptions": {
"extendedDiagnostics": {
"checks": {
"nullishCoalescingNotNullable": "suppress",
"optionalChainNotNullable": "suppress"
}
}
}
}
+2 -1
View File
@@ -10,7 +10,8 @@
"moduleResolution": "bundler",
"isolatedModules": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
+4 -1
View File
@@ -9,7 +9,10 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"noUncheckedSideEffectImports": false,
"types": ["*"],
"ignoreDeprecations": "6.0"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
+19
View File
@@ -18,7 +18,26 @@
"bundle": true,
"thirdParty": true,
"generatePackageJson": true
},
"inputs": [
"default",
"^default",
"{workspaceRoot}/pnpm-workspace.yaml",
{
"json": "{workspaceRoot}/package.json",
"fields": [
"packageManager",
"pnpm.onlyBuiltDependencies",
"pnpm.neverBuiltDependencies",
"pnpm.allowBuilds",
"pnpm.supportedArchitectures",
"pnpm.patchedDependencies"
]
},
{
"runtime": "node -e \"try{console.log('pnpm major '+require('child_process').execSync('pnpm --version',{stdio:['ignore','pipe','ignore']}).toString().trim().split('.')[0])}catch{console.log('pnpm major unavailable')}\""
}
]
},
"serve": {
"continuous": true,
+2 -1
View File
@@ -8,7 +8,8 @@
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
"skipLibCheck": true,
"ignoreDeprecations": "6.0"
},
"include": ["src/**/*.ts"]
}
+170 -1
View File
@@ -4,6 +4,12 @@ import { expect, test } from './fixtures';
const fixturePath = join(__dirname, 'fixtures/test.m3u');
test('@web @m3u basic playlist import flow', async ({ page }) => {
// The fixture's placeholder media hosts must not delay navigation or make
// persistence coverage depend on external DNS and asset availability.
await page.route(
/^https?:\/\/(?:channel\.icons\.url|example\.channels|xml-url)\//,
(route) => route.abort()
);
await page.goto('/');
// Basic checks
@@ -29,6 +35,15 @@ test('@web @m3u basic playlist import flow', async ({ page }) => {
await expect(page.getByText('4 channels')).toBeVisible();
await expect(page.getByText('1. Channel 1')).toBeVisible();
await expect(page.getByText('4. HappyKids TV')).toBeVisible();
const importedPlaylistUrl = page.url();
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page).toHaveURL(importedPlaylistUrl);
await expect(page.getByText('test', { exact: true })).toBeVisible();
await expect(page.getByText('4 channels')).toBeVisible();
await expect(page.getByText('1. Channel 1')).toBeVisible();
await expect(page.getByText('4. HappyKids TV')).toBeVisible();
});
test('@web @auto-detect pasted provider message prefills the Xtream form', async ({
@@ -110,7 +125,161 @@ test('@web @auto-detect keeps the pasted message when switching methods', async
);
});
test('@web keyboard shortcuts help opens from question mark', async ({ page }) => {
test('@web @m3u reads an existing v1 IndexedDB playlist after application upgrade', async ({
page,
}) => {
const playlistId = 'legacy-indexeddb-playlist';
// Seed before Angular boots so the application must open the old database.
await page.route('**/__indexeddb-seed__', (route) =>
route.fulfill({ contentType: 'text/html', body: '<!doctype html>' })
);
await page.route('https://legacy.example.invalid/**', (route) =>
route.abort()
);
await page.goto('/__indexeddb-seed__');
await page.evaluate(
(id) =>
new Promise<void>((resolve, reject) => {
const request = indexedDB.open('iptvnator', 1);
request.onerror = () => reject(request.error);
request.onupgradeneeded = () => {
const store = request.result.createObjectStore(
'playlists',
{
keyPath: '_id',
autoIncrement: false,
}
);
// Freeze the historical schema independently of app config.
for (const name of [
'_id',
'filename',
'title',
'count',
'playlist',
'importDate',
'lastUsage',
'favorites',
'recentlyViewed',
'autoRefresh',
'url',
'filePath',
]) {
store.createIndex(name, name, { unique: false });
}
};
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction(
'playlists',
'readwrite'
);
transaction.onabort = () => {
db.close();
reject(transaction.error);
};
transaction.oncomplete = () => {
db.close();
resolve();
};
transaction.objectStore('playlists').add({
_id: id,
title: 'Legacy playlist',
filename: 'legacy.m3u',
importDate: '2025-01-01T00:00:00.000Z',
lastUsage: '2025-01-01T00:00:00.000Z',
count: 1,
autoRefresh: false,
favorites: [],
recentlyViewed: [],
playlist: {
header: { attrs: {}, raw: '#EXTM3U' },
items: [
{
id: 'legacy-channel',
name: 'Legacy channel',
url: 'https://legacy.example.invalid/live.m3u8',
group: { title: 'News' },
tvg: {
id: '',
name: '',
url: '',
logo: '',
rec: '',
},
http: {
referrer: '',
'user-agent': '',
origin: '',
},
radio: 'false',
},
],
},
});
};
}),
playlistId
);
const playlistPath = `/workspace/playlists/${playlistId}/all`;
await page.goto(playlistPath, { waitUntil: 'domcontentloaded' });
await expect(
page.getByText('Legacy playlist', { exact: true })
).toBeVisible();
await expect(
page.getByText('1. Legacy channel', { exact: true })
).toBeVisible();
await page.reload({ waitUntil: 'domcontentloaded' });
await expect(page).toHaveURL(new RegExp(`${playlistPath}$`));
await expect(
page.getByText('Legacy playlist', { exact: true })
).toBeVisible();
await expect(
page.getByText('1. Legacy channel', { exact: true })
).toBeVisible();
const persisted = await page.evaluate(
(id) =>
new Promise((resolve, reject) => {
const request = indexedDB.open('iptvnator');
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
const transaction = db.transaction('playlists', 'readonly');
const store = transaction.objectStore('playlists');
const record = store.get(id);
transaction.onabort = () => {
db.close();
reject(transaction.error);
};
transaction.oncomplete = () => {
resolve({
version: db.version,
keyPath: store.keyPath,
autoIncrement: store.autoIncrement,
id: record.result?._id,
channel: record.result?.playlist.items[0].name,
});
db.close();
};
};
}),
playlistId
);
expect(persisted).toEqual({
version: 1,
keyPath: '_id',
autoIncrement: false,
id: playlistId,
channel: 'Legacy channel',
});
});
test('@web keyboard shortcuts help opens from question mark', async ({
page,
}) => {
await page.goto('/');
await page.getByRole('button', { name: 'Open keyboard shortcuts' }).focus();
+24
View File
@@ -66,6 +66,18 @@ test.skip(
'DASH ClearKey coverage targets Chromium'
);
test.beforeEach(async ({ page }) => {
await page.addInitScript(() => {
const play = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function () {
// Keep real decoding while avoiding the host's audio-output clock.
// Muting must happen before play, including replacement players.
this.muted = true;
return play.call(this);
};
});
});
async function serveDashFixtures(page: Page): Promise<void> {
await page.route(`${FIXTURE_HOST}/**`, async (route) => {
const url = new URL(route.request().url());
@@ -150,7 +162,19 @@ test('@web @m3u @dash ClearKey and clear DASH channels play inline', async ({
await page.getByText('1. ClearKey DASH').click();
await expectVideoPlaying(page);
const clearKeyVideo = await page
.locator('app-web-player-view video')
.first()
.elementHandle();
if (!clearKeyVideo) {
throw new Error('Missing playing ClearKey video');
}
await page.getByText('2. Clear DASH').click();
// The new source mounts asynchronously; never accept the old video's time.
await page.waitForFunction(
(previous) => !previous.isConnected,
clearKeyVideo
);
await expectVideoPlaying(page);
});
+2 -1
View File
@@ -9,7 +9,8 @@
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"ignoreDeprecations": "6.0"
},
"include": [
"**/*.ts",
+1 -1
View File
@@ -1,4 +1,4 @@
import { nxPreset } from '@nx/jest/preset.js';
import { nxPreset } from '@nx/jest/preset';
import { createEsmPreset } from 'jest-preset-angular/presets/index.js';
const angularEsmPreset = createEsmPreset({
@@ -5,6 +5,7 @@ import {
OnInit,
signal,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
@@ -101,6 +102,8 @@ import { AppUpdateReleaseNotesDialogComponent } from './settings/app-update-rele
</section>
}
`,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
.app-update-notification {
+3
View File
@@ -5,6 +5,7 @@ import {
inject,
OnInit,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router, RouterOutlet } from '@angular/router';
@@ -48,6 +49,8 @@ const debugAppComponent = createDevLogger('AppComponent');
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
AppStartupStatusComponent,
AppUpdateNotificationPanelComponent,
+2 -1
View File
@@ -2,6 +2,7 @@ import {
HttpClient,
provideHttpClient,
withInterceptorsFromDi,
withXhr,
} from '@angular/common/http';
import {
FullscreenOverlayContainer,
@@ -114,7 +115,7 @@ export const appConfig: ApplicationConfig = {
// fullscreen channel panel's sort/context menus and the controls'
// tooltips render while the player is fullscreen.
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
provideHttpClient(withInterceptorsFromDi()),
provideHttpClient(withXhr(), withInterceptorsFromDi()),
provideStore({
router: routerReducer,
playlistState: playlistReducer,
@@ -6,6 +6,7 @@ import {
SecurityContext,
signal,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { DatePipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
@@ -76,7 +77,9 @@ function decorateReleaseNotesHtml(html: string): string {
</button>
<div class="release-notes-dialog__version">
<strong>{{ notes()?.releaseName || notes()?.tagName }}</strong>
<strong>{{
notes()?.releaseName || notes()?.tagName
}}</strong>
@if (notes()?.publishedAt; as publishedAt) {
<span>{{ publishedAt | date: 'mediumDate' }}</span>
}
@@ -120,6 +123,8 @@ function decorateReleaseNotesHtml(html: string): string {
</button>
</mat-dialog-actions>
`,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
.release-notes-dialog {
@@ -210,9 +215,8 @@ function decorateReleaseNotesHtml(html: string): string {
],
})
export class AppUpdateReleaseNotesDialogComponent implements OnInit {
private readonly data = inject<AppUpdateReleaseNotesDialogData>(
MAT_DIALOG_DATA
);
private readonly data =
inject<AppUpdateReleaseNotesDialogData>(MAT_DIALOG_DATA);
private readonly dialogRef = inject(
MatDialogRef<AppUpdateReleaseNotesDialogComponent>
);
@@ -271,7 +275,9 @@ export class AppUpdateReleaseNotesDialogComponent implements OnInit {
await window.electron.getAppUpdateReleaseNotes(request)
);
} catch (error) {
this.error.set(error instanceof Error ? error.message : String(error));
this.error.set(
error instanceof Error ? error.message : String(error)
);
} finally {
this.loading.set(false);
}
@@ -4,6 +4,7 @@ import {
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
@@ -18,6 +19,8 @@ import {
imports: [MatButtonModule, MatIconModule, TranslateModule],
templateUrl: './settings-about-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
':host { display: contents; }',
'.version-block .build-commit { opacity: 0.65; font-size: 0.85em; }',
@@ -28,7 +31,9 @@ export class SettingsAboutSectionComponent {
readonly version = input<string | undefined>();
readonly buildCommit = input<string | undefined>();
readonly updateMessage = input<string | undefined>();
readonly appUpdateStatus = input<ElectronBridgeAppUpdateStatus | null>(null);
readonly appUpdateStatus = input<ElectronBridgeAppUpdateStatus | null>(
null
);
readonly buildCommitShort = computed(() => {
const commit = this.buildCommit()?.trim();
@@ -1,4 +1,10 @@
import { Component, input, output, ViewEncapsulation } from '@angular/core';
import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -14,6 +20,8 @@ import { TranslateModule } from '@ngx-translate/core';
],
templateUrl: './settings-backup-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'],
})
export class SettingsBackupSectionComponent {
@@ -1,5 +1,10 @@
import { CommonModule } from '@angular/common';
import { Component, input, ViewEncapsulation } from '@angular/core';
import {
Component,
input,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
@@ -16,6 +21,8 @@ import { TranslateModule } from '@ngx-translate/core';
],
templateUrl: './settings-dashboard-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'],
})
export class SettingsDashboardSectionComponent {
@@ -1,5 +1,10 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import {
Component,
computed,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
@@ -25,6 +30,8 @@ type SettingsDeleteSummaryItem = {
selector: 'app-settings-delete-all-playlists-dialog',
templateUrl: './settings-delete-all-playlists-dialog.component.html',
styleUrls: ['./settings-delete-all-playlists-dialog.component.scss'],
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
CommonModule,
MatButtonModule,
@@ -1,4 +1,10 @@
import { Component, input, output, ViewEncapsulation } from '@angular/core';
import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormArray, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -28,6 +34,8 @@ import { EpgViewModeOption } from './settings.models';
],
templateUrl: './settings-epg-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'],
})
export class SettingsEpgSectionComponent {
@@ -1,5 +1,11 @@
import { CommonModule } from '@angular/common';
import { Component, input, output, ViewEncapsulation } from '@angular/core';
import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -27,6 +33,8 @@ import {
],
templateUrl: './settings-general-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'],
})
export class SettingsGeneralSectionComponent {
@@ -1,5 +1,11 @@
import { CommonModule } from '@angular/common';
import { Component, input, output, ViewEncapsulation } from '@angular/core';
import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -30,6 +36,8 @@ import { SettingsPlayerOption } from './settings.models';
],
templateUrl: './settings-playback-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'],
})
export class SettingsPlaybackSectionComponent {
@@ -1,4 +1,10 @@
import { Component, input, output, ViewEncapsulation } from '@angular/core';
import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -24,6 +30,8 @@ import { QRCodeComponent } from 'angularx-qrcode';
],
templateUrl: './settings-remote-control-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'],
})
export class SettingsRemoteControlSectionComponent {
@@ -1,4 +1,10 @@
import { Component, input, output, ViewEncapsulation } from '@angular/core';
import {
Component,
input,
output,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
@@ -15,6 +21,8 @@ import { SettingsPlaylistDeleteSummary } from './settings.models';
],
templateUrl: './settings-reset-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [':host { display: contents; }'],
})
export class SettingsResetSectionComponent {
@@ -4,6 +4,7 @@ import {
input,
signal,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@@ -32,6 +33,8 @@ type TmdbKeyTestState = 'idle' | 'testing' | 'success' | 'error';
],
templateUrl: './settings-tmdb-section.component.html',
encapsulation: ViewEncapsulation.None,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
app-settings-tmdb-section {
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core';
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
@@ -38,6 +38,8 @@ export interface SettingsUnsavedChangesDialogData {
}
`,
],
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
template: `
<h2 mat-dialog-title>
{{ 'SETTINGS.UNSAVED_DIALOG_TITLE' | translate }}
@@ -472,7 +472,11 @@ describe('SettingsComponent form', () => {
const webFixture = TestBed.createComponent(SettingsComponent);
const webComponent = webFixture.componentInstance;
stubSettingsSideEffects(webComponent);
const loadSettings = jest.spyOn(webComponent.form, 'loadSettings');
webFixture.detectChanges();
expect(loadSettings).toHaveBeenCalledTimes(1);
// Await hydration before editing; Zone cannot track native awaits.
await loadSettings.mock.results[0].value;
await webFixture.whenStable();
webComponent.settingsForm.patchValue({ theme: Theme.DarkTheme });
@@ -78,6 +78,10 @@ describe('SettingsComponent', () => {
const lifecycleFixture = TestBed.createComponent(SettingsComponent);
const lifecycleComponent = lifecycleFixture.componentInstance;
stubSettingsSideEffects(lifecycleComponent);
const loadSettings = jest.spyOn(
lifecycleComponent.form,
'loadSettings'
);
const appUpdateInit = jest.spyOn(lifecycleComponent.appUpdate, 'init');
const appUpdateDispose = jest.spyOn(
lifecycleComponent.appUpdate,
@@ -89,6 +93,9 @@ describe('SettingsComponent', () => {
);
lifecycleFixture.detectChanges();
expect(loadSettings).toHaveBeenCalledTimes(1);
// Let ngOnInit resume after its native promise, which Zone cannot track.
await loadSettings.mock.results[0].value;
await lifecycleFixture.whenStable();
expect(appUpdateInit).toHaveBeenCalledTimes(1);
@@ -197,7 +204,10 @@ describe('SettingsComponent', () => {
it('save-and-leave persists before allowing the navigation', async () => {
component.settingsForm.markAsDirty();
jest.spyOn(component.epg, 'fetchConfiguredEpg').mockImplementation();
jest.spyOn(
component.epg,
'fetchConfiguredEpg'
).mockImplementation();
answerDialogWith('save');
await expect(
@@ -7,6 +7,7 @@ import {
OnDestroy,
OnInit,
ViewEncapsulation,
ChangeDetectionStrategy,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ReactiveFormsModule } from '@angular/forms';
@@ -93,6 +94,8 @@ export const SETTINGS_DEFAULT_SECTION = 'general';
SettingsResetSectionComponent,
SettingsTmdbSectionComponent,
],
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
providers: [
SettingsAppUpdateFacade,
SettingsBackupFacade,
+5 -2
View File
@@ -1,4 +1,7 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone/index.mjs';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless/index.mjs';
import { installDuplicateVideoJsQualityLevelsWarnFilter } from '@iptvnator/shared/testing';
Object.defineProperty(globalThis, 'jest', {
@@ -25,7 +28,7 @@ if (!Element.prototype.animate) {
installDuplicateVideoJsQualityLevelsWarnFilter();
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+9 -1
View File
@@ -12,5 +12,13 @@
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.stub.ts"
]
],
"angularCompilerOptions": {
"extendedDiagnostics": {
"checks": {
"nullishCoalescingNotNullable": "suppress",
"optionalChainNotNullable": "suppress"
}
}
}
}
+2 -1
View File
@@ -15,7 +15,8 @@
"resolveJsonModule": true,
"strict": true,
"skipLibCheck": true,
"useDefineForClassFields": false
"useDefineForClassFields": false,
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
+3 -3
View File
@@ -3,9 +3,9 @@
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
"@/*": ["./src/*"]
},
"ignoreDeprecations": "6.0"
}
}
+4 -1
View File
@@ -9,7 +9,10 @@
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"noUncheckedSideEffectImports": false,
"types": ["*"],
"ignoreDeprecations": "6.0"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
+2 -1
View File
@@ -6,7 +6,8 @@
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"],
"strict": true
"strict": true,
"ignoreDeprecations": "6.0"
},
"include": [
"jest.config.ts",
+1 -1
View File
@@ -8,7 +8,7 @@ RUN apk add --no-cache python3 make g++ git
WORKDIR /usr/src/app
COPY .npmrc ./
COPY package.json pnpm-lock.yaml ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY patches ./patches
# Node 25 unbundled Corepack, so `corepack enable` cannot be the way pnpm gets
+61 -14
View File
@@ -56,23 +56,70 @@ Omit `pnpm nx migrate --run-migrations` when the first command reports that no
migrations exist. Major Nx updates always use this manual workflow and the
resulting PR runs the full CI pipeline.
## Angular 22 Toolchain Compatibility
Angular framework/Material 22.1 and CLI/build 22.1 use Nx 23.2 and TypeScript
6.0. Framework and CLI patch numbers may differ; keep framework packages aligned
in both dependency sections and all official Nx packages on one exact version.
Use `.nvmrc` for development/CI; supported LTS Node ranges are
`^22.22.3 || ^24.15.0`. Docker uses the supported Node 24 line.
The Angular 22 migration explicitly preserves the old default change detection
with `ChangeDetectionStrategy.Eager`. Existing OnPush components and
`provideZoneChangeDetection` remain in their existing modes.
Existing eager components under Angular ESLint carry a line-scoped explanation
for the newly recommended OnPush rule; new components keep that rule enabled.
Adopting OnPush in those components requires a separate behavior review.
TypeScript 6 migration settings preserve compiler defaults and per-project output roots;
`ignoreDeprecations: "6.0"` temporarily supports legacy Node test configs and
`baseUrl`, rather than changing the whole workspace to browser resolution.
Two third-party packages need scoped compatibility bridges in
`pnpm-workspace.yaml`:
- `nx-electron@22.0.0` still declares Nx 22 peers. Its existing package-layout
patch also changes the removed `@nx/js/src/utils/buildable-libs-utils` import
to `@nx/js/internal`. A package extension supplies its undeclared runtime
dependency `webpack-node-externals`. The packaging suite loads every executor
and checks the single application manifest. Remove these bridges only after
the upstream package declares and implements the current Nx APIs.
- `ngx-indexed-db@22.0.0` declares Angular `<22`, but its used API remains
compatible. Its allowance is scoped to the exact Angular version and must be
revalidated on upgrades. `apps/web-e2e/src/basic.e2e.ts` opens a historical
`iptvnator` v1 database, retains the `_id` key path, and checks reload
persistence. The shared interfaces package declares the same library version.
Remove the allowance when upstream includes the installed Angular version.
Jest uses root Babel 7 while Angular build retains its own Babel 8. The website
uses Astro 7 with the root TypeScript 6; no Astro-specific compiler package
extension is needed. Keep the website build in toolchain validation.
The website's alias uses a relative `./src/*` target without the deprecated
`baseUrl` option.
Electron's webpack build resolves `ts-loader` 9.6.2 through the existing
`nx-electron` version range. Keep at least 9.5.7: earlier versions clear
`rootDir` in transpile-only mode and fail with TypeScript 6 (TS5011/TS6059).
Angular unit-test setup explicitly uses `setupZonelessTestEnv` and separately
imports `zone.js/testing` for existing `fakeAsync` helpers. This preserves the
Angular 21 scheduler used by jest-preset-angular 15: its `setupZoneTestEnv` loaded
Zone but did not provide zone-based change detection on Angular 20+. Preset 17
does provide it, which disables automatic fixture rendering and changes
`whenStable()` behavior. Production applications continue to use their existing
`provideZoneChangeDetection`; browser and Electron E2E validate that runtime.
## Vite Dev-Server Patch
Angular's development builder currently resolves Vite `7.3.6`. That release's
asset and worker transform prefilters can catastrophically backtrack on a large
generated chunk containing unrelated `new URL...` expressions while searching
for a valid `new URL(..., import.meta.url)` construct. The Electron development
server can then fail a lazy chunk request with `Maximum call stack size
exceeded`, even though static builds succeed because they do not pass emitted
chunks through Vite's request-time plugin container.
Angular's development builder resolves Vite `8.1.5`. It contains upstream precise
asset/worker URL matchers, but also uses them as raw-code prefilters. Those
prefilters reject valid expressions containing comments before the handlers can
strip the comments and apply the precise matchers.
`patches/vite@7.3.6.patch` backports Vite's upstream precise-matcher fix from
[vitejs/vite#21800](https://github.com/vitejs/vite/pull/21800). Bounded
prefilters keep the request-time scan linear while allowing comment-bearing
asset and worker expressions to reach the precise matcher after Vite strips
comments. Keep the patch while the supported Angular toolchain resolves Vite
`7.3.6`; remove it only after the resolved Vite contains the upstream fix. Run
the regression check after any related manifest or lockfile update:
`patches/vite@8.1.5.patch` adds bounded asset/worker prefilters and uses the asset
prefilter for bundled URL rewriting too. The precise handler matchers remain
unchanged. This preserves the earlier protection against expensive scanning of
large false-positive chunks and retains comment-bearing expressions. Remove the
patch only when the resolved upstream Vite passes the same behavior checks:
```bash
pnpm run deps:vite:test
+6 -5
View File
@@ -325,6 +325,7 @@ handlers in `apps/electron-backend/src/app/events/window.events.ts`):
`leave-html-full-screen` would un-hide the controls over a window that
is still fullscreen. A fullscreen launch (below) or F11 followed by the
player's `F` → `Esc` makes that path routine on Windows/Linux.
3. `WINDOW:TOGGLE_FULLSCREEN` toggles OS-level fullscreen (`setFullScreen`)
and, like the maximize toggle, reports the requested state and leaves
the `WINDOW:STATE_CHANGED` push authoritative. Because the transition is
@@ -439,15 +440,15 @@ Toolchain notes for the Electron 43 baseline:
implicit `node-gyp rebuild` bypasses those binaries and makes installation
depend on the host compiler toolchain. The old `node-abi` override belonged
to v12's removed `prebuild-install` path and is no longer required.
2. Local development supports **Node 22.13–22.x or Node >= 24**, declared in
`engines` as `^22.13.0 || >=24.0.0`.
The direct `@faker-js/faker` dependency and current lint tooling require
that floor. `electron-builder` 26.15.7 also pulls `@electron/rebuild` 4,
2. Local development supports **Node 22.22.3–22.x or 24.15.0–24.x**, declared in
`engines` as `^22.22.3 || ^24.15.0`. Use `.nvmrc` (currently 22.23.2) for
development and CI. Angular 22 sets this supported LTS range and requires
TypeScript `>=6.0 <6.1`. `electron-builder` 26.15.7 also pulls `@electron/rebuild` 4,
which requires Node 22.12 or newer, and the root `postinstall` runs
`install-app-deps` on every `pnpm install`. The 26.15.7 minimum also
carries the v26 backport that fully extracts the Snap template's `.tar.7z`
payload; 26.15.0–26.15.6 can
produce a Snap that is missing `desktop-init.sh`. CI already runs Node 22.
produce a Snap that is missing `desktop-init.sh`.
3. Dependabot keeps Electron, native database, packaging, EPG parser, and
version-locked Shaka/mpegts updates out of the shared npm minor/patch group.
Those dependencies require standalone PRs so their dedicated package,
+1 -1
View File
@@ -1,4 +1,4 @@
import { nxPreset } from '@nx/jest/preset.js';
import { nxPreset } from '@nx/jest/preset';
import { createEsmPreset } from 'jest-preset-angular/presets/index.js';
const angularEsmPreset = createEsmPreset({
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -9,7 +9,8 @@
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"files": [],
"include": [],
@@ -6,6 +6,7 @@ import {
signal,
ViewEncapsulation,
viewChild,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
@@ -70,6 +71,7 @@ const METHOD_BY_CANDIDATE_KIND: Record<
selector: 'app-add-playlist',
templateUrl: './add-playlist-dialog.component.html',
styleUrl: './add-playlist-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
encapsulation: ViewEncapsulation.None,
})
export class AddPlaylistDialogComponent {
@@ -98,8 +100,9 @@ export class AddPlaylistDialogComponent {
* the prefill is applied by an effect that re-runs once the child's
* viewChild signal resolves, then clears this.
*/
private readonly pendingPrefill =
signal<ProviderImportCandidate | null>(null);
private readonly pendingPrefill = signal<ProviderImportCandidate | null>(
null
);
/**
* Survives the auto-detect surface being destroyed by a method switch, so
@@ -1,4 +1,11 @@
import { Component, computed, input, OnInit, output } from '@angular/core';
import {
Component,
computed,
input,
OnInit,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
@@ -139,6 +146,7 @@ function maskUrlQueryPasswords(url: string): string {
selector: 'app-auto-import',
templateUrl: './auto-import.component.html',
styleUrl: './auto-import.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
MatButtonModule,
MatFormFieldModule,
@@ -228,10 +236,7 @@ export class AutoImportComponent implements OnInit {
);
break;
case 'xtream':
push(
'HOME.XTREAM_PLAYLIST.SERVER_URL',
candidate.serverUrl
);
push('HOME.XTREAM_PLAYLIST.SERVER_URL', candidate.serverUrl);
push('HOME.XTREAM_PLAYLIST.USERNAME', candidate.username);
push(
'HOME.XTREAM_PLAYLIST.PASSWORD',
@@ -239,14 +244,8 @@ export class AutoImportComponent implements OnInit {
);
break;
case 'stalker':
push(
'HOME.STALKER_PORTAL.SERVER_URL',
candidate.portalUrl
);
push(
'HOME.STALKER_PORTAL.MAC_ADDRESS',
candidate.macAddress
);
push('HOME.STALKER_PORTAL.SERVER_URL', candidate.portalUrl);
push('HOME.STALKER_PORTAL.MAC_ADDRESS', candidate.macAddress);
push(
'HOME.STALKER_PORTAL.SERIAL_NUMBER',
candidate.serialNumber
@@ -1,4 +1,11 @@
import { Component, EventEmitter, Output, inject, signal } from '@angular/core';
import {
Component,
EventEmitter,
Output,
inject,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core';
import { PlaylistFileImportService } from '@iptvnator/playlist/shared/util';
@@ -11,6 +18,7 @@ const KB = 1024;
imports: [DragDropFileUploadDirective, MatIconModule, TranslatePipe],
selector: 'app-file-upload',
templateUrl: './file-upload.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrls: ['./file-upload.component.scss'],
})
export class FileUploadComponent {
@@ -1,4 +1,10 @@
import { Component, inject, output, signal } from '@angular/core';
import {
Component,
inject,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import {
FormControl,
FormGroup,
@@ -59,6 +65,7 @@ interface StalkerSettledIdentity {
],
selector: 'app-stalker-portal-import',
templateUrl: './stalker-portal-import.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
:host {
@@ -1,4 +1,9 @@
import { Component, EventEmitter, Output } from '@angular/core';
import {
Component,
EventEmitter,
Output,
ChangeDetectionStrategy,
} from '@angular/core';
import {
FormControl,
FormGroup,
@@ -12,6 +17,7 @@ import { TranslatePipe } from '@ngx-translate/core';
selector: 'app-text-import',
templateUrl: './text-import.component.html',
styleUrls: ['./text-import.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [MatInputModule, ReactiveFormsModule, TranslatePipe],
})
export class TextImportComponent {
@@ -1,4 +1,9 @@
import { Component, inject, OnInit } from '@angular/core';
import {
Component,
inject,
OnInit,
ChangeDetectionStrategy,
} from '@angular/core';
import {
FormBuilder,
FormControl,
@@ -14,6 +19,7 @@ import { RuntimeCapabilitiesService } from '@iptvnator/services';
@Component({
selector: 'app-url-upload',
templateUrl: './url-upload.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
MatFormFieldModule,
MatInputModule,
@@ -1,4 +1,10 @@
import { Component, EventEmitter, Output, inject } from '@angular/core';
import {
Component,
EventEmitter,
Output,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import {
AbstractControl,
FormControl,
@@ -47,6 +53,7 @@ function xtreamServerUrlValidator(
],
selector: 'app-xtream-code-import',
templateUrl: './xtream-code-import.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
:host {
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
@@ -7,6 +7,7 @@ import {
output,
signal,
untracked,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatIcon } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core';
@@ -59,6 +60,7 @@ import { M3uVodMetadataService } from './m3u-vod-metadata.service';
TranslatePipe,
],
templateUrl: './m3u-vod-detail.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrls: ['./m3u-vod-detail.component.scss'],
})
export class M3uVodDetailComponent {
@@ -105,7 +105,9 @@
@if (activeChannel.radio === 'true') {
<app-audio-player
[url]="activeChannel.url"
[icon]="activeChannel?.tvg?.logo!"
[icon]="
$safeNavigationMigration(activeChannel?.tvg?.logo)!
"
[channelName]="displayChannelName()"
[volume]="volume()"
(volumeChange)="onInlineVolumeChange($event)"
@@ -14,6 +14,7 @@ import {
signal,
untracked,
viewChild,
ChangeDetectionStrategy,
} from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
@@ -226,6 +227,7 @@ function isInsideScrollableRegion(
{ provide: EPG_GUIDE_SOURCE, useExisting: M3uEpgGuideSourceService },
],
templateUrl: './video-player.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './video-player.component.scss',
})
export class VideoPlayerComponent
@@ -1,14 +1,16 @@
import { Component, input, output } from '@angular/core';
import {
Component,
input,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core';
import type { PlaylistType } from '../../add-playlist-menu/playlist-type';
export type EmptyStateType =
| 'welcome-dashboard'
| 'welcome-sources'
| 'no-results'
| 'no-data';
'welcome-dashboard' | 'welcome-sources' | 'no-results' | 'no-data';
interface FeatureCard {
icon: string;
@@ -100,6 +102,7 @@ const SOURCE_CARDS: readonly SourceCard[] = [
'./empty-state.responsive.scss',
'./empty-state.themes.scss',
],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [MatButtonModule, MatIcon, TranslatePipe],
})
export class EmptyStateComponent {
@@ -171,15 +171,9 @@ describe('PlaylistInfoComponent', () => {
currentLang: 'en',
get: jest.fn((key: string) => of(key)),
instant: jest.fn((key: string) => key),
onDefaultLangChange: of({
lang: 'en',
translations: {},
}),
onLangChange: of({ lang: 'en', translations: {} }),
onTranslationChange: of({
lang: 'en',
translations: {},
}),
onDefaultLangChange: new Subject(),
onLangChange: new Subject(),
onTranslationChange: new Subject(),
},
},
{
@@ -1,6 +1,12 @@
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
import { DatePipe } from '@angular/common';
import { Component, DestroyRef, inject, signal } from '@angular/core';
import {
Component,
DestroyRef,
inject,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import {
FormControl,
ReactiveFormsModule,
@@ -159,6 +165,7 @@ type DesktopFileSaveBridge = Pick<
`,
],
providers: [DatePipe],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
ClipboardModule,
MatButton,
@@ -12,6 +12,7 @@ import {
inject,
input,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatIconButton } from '@angular/material/button';
@@ -33,6 +34,7 @@ import { PlaylistMeta } from '@iptvnator/shared/interfaces';
selector: 'app-playlist-item',
templateUrl: './playlist-item.component.html',
styleUrls: ['./playlist-item.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
SourceHealthIndicatorComponent,
DatePipe,
@@ -1,4 +1,11 @@
import { Component, DestroyRef, computed, effect, inject } from '@angular/core';
import {
Component,
DestroyRef,
computed,
effect,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { DatePipe } from '@angular/common';
import {
MAT_DIALOG_DATA,
@@ -107,7 +114,9 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
@if (entry.status === 'ready') {
<span>{{
'SOURCE_HEALTH.REASON.' +
entry.health?.reason | translate
$safeNavigationMigration(
entry.health?.reason
) | translate
}}</span>
} @else {
<span>{{
@@ -117,8 +126,9 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
}
@if (entry.health?.checkedAt) {
<time>{{
$safeNavigationMigration(
entry.health?.checkedAt
| date: 'shortTime'
) | date: 'shortTime'
}}</time>
}
@if (entry.warning) {
@@ -170,6 +180,7 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
</button>
}
</mat-dialog-actions>`,
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
:host {
@@ -6,6 +6,7 @@ import {
inject,
input,
untracked,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslateService } from '@ngx-translate/core';
@@ -28,6 +29,7 @@ import { PlaylistMeta, sourceHealthType } from '@iptvnator/shared/interfaces';
role="img"
></span>
}`,
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
:host {
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
@@ -1,4 +1,10 @@
import { Component, input, output, signal } from '@angular/core';
import {
Component,
input,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
@@ -21,6 +27,7 @@ import { CategoryContentViewComponent } from './category-content-view.component'
@Component({
selector: 'app-grid-list',
standalone: true,
changeDetection: ChangeDetectionStrategy.Eager,
template: '',
})
class MockGridListComponent {
@@ -37,6 +44,7 @@ class MockGridListComponent {
@Component({
selector: 'app-playlist-error-view',
standalone: true,
changeDetection: ChangeDetectionStrategy.Eager,
template: '',
})
class MockPlaylistErrorViewComponent {
@@ -48,6 +56,7 @@ class MockPlaylistErrorViewComponent {
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.Eager,
template: '',
})
class MockDetailComponent {
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
@@ -312,7 +312,10 @@
{{
episodeTitle(
episode.item,
episode.episodeMetadata?.title
$safeNavigationMigration(
episode.episodeMetadata
?.title
)
)
}}
</strong>
@@ -360,7 +363,10 @@
' ' +
episodeTitle(
episode.item,
episode.episodeMetadata?.title
$safeNavigationMigration(
episode.episodeMetadata
?.title
)
)
"
(click)="
@@ -390,7 +396,10 @@
': ' +
episodeTitle(
episode.item,
episode.episodeMetadata?.title
$safeNavigationMigration(
episode.episodeMetadata
?.title
)
)
"
>
@@ -418,9 +427,11 @@
' ' +
episodeTitle(
episode.item,
$safeNavigationMigration(
episode.episodeMetadata
?.title
)
)
"
(click)="
runFileAction(
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
@@ -1,4 +1,10 @@
import { Component, inject, input, output } from '@angular/core';
import {
Component,
inject,
input,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
@@ -50,6 +56,7 @@ export interface FavoriteLayoutItem {
'./favorites-layout.component.scss',
'../../styles/portal-sidebar.scss',
],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [
CategoryViewComponent,
ContentCardComponent,
@@ -1,4 +1,9 @@
import { Component, inject, input } from '@angular/core';
import {
Component,
inject,
input,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
@@ -14,6 +19,7 @@ import { PlaylistActions } from '@iptvnator/m3u-state';
selector: 'app-playlist-error-view',
templateUrl: './playlist-error-view.component.html',
styleUrls: ['./playlist-error-view.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule],
})
export class PlaylistErrorViewComponent {
@@ -6,6 +6,7 @@ import {
model,
Output,
ViewChild,
ChangeDetectionStrategy,
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatCheckboxModule } from '@angular/material/checkbox';
@@ -38,6 +39,7 @@ export interface SearchFilter {
TranslatePipe,
],
templateUrl: './search-form.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
.search-container {
@@ -1,4 +1,4 @@
import { Component, signal } from '@angular/core';
import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { InfiniteScrollDirective } from './infinite-scroll.directive';
@@ -13,6 +13,7 @@ import { InfiniteScrollDirective } from './infinite-scroll.directive';
[infiniteResetKey]="resetKey()"
(infiniteLoadMore)="onLoadMore()"
></div>`,
changeDetection: ChangeDetectionStrategy.Eager,
imports: [InfiniteScrollDirective],
})
class TestHostComponent {
@@ -1,4 +1,9 @@
import { Component, inject, input } from '@angular/core';
import {
Component,
inject,
input,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatIcon } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatTooltip } from '@angular/material/tooltip';
@@ -8,12 +13,16 @@ import {
RouterLink,
RouterLinkActive,
} from '@angular/router';
import { PortalRailLink, PortalRailSection } from '@iptvnator/portal/shared/util';
import {
PortalRailLink,
PortalRailSection,
} from '@iptvnator/portal/shared/util';
@Component({
selector: 'app-portal-rail-links',
imports: [MatIcon, MatListModule, MatTooltip, RouterLink, RouterLinkActive],
templateUrl: './portal-rail-links.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './portal-rail-links.component.scss',
})
export class PortalRailLinksComponent {
@@ -27,7 +36,9 @@ export class PortalRailLinksComponent {
readonly variant = input<'list' | 'rail'>('list');
resolveIcon(link: PortalRailLink): string {
const normalizedPath = link.path.map((segment) => String(segment)).join('/');
const normalizedPath = link.path
.map((segment) => String(segment))
.join('/');
if (normalizedPath.includes('/workspace/sources')) {
return 'playlist_play';
}
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
+5 -2
View File
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+2 -1
View File
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
@@ -10,7 +10,8 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
"module": "preserve",
"ignoreDeprecations": "6.0"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
@@ -1,5 +1,11 @@
import { Location } from '@angular/common';
import { Component, input, output, signal } from '@angular/core';
import {
Component,
input,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MatSnackBar } from '@angular/material/snack-bar';
@@ -33,7 +39,11 @@ function deferred<T>() {
return { promise, reject, resolve };
}
@Component({ selector: 'app-vod-details', template: '' })
@Component({
selector: 'app-vod-details',
changeDetection: ChangeDetectionStrategy.Eager,
template: '',
})
class StubVodDetailsComponent {
readonly playbackSessionKey = input.required<string>();
readonly item = input.required<unknown>();
@@ -53,7 +63,11 @@ class StubVodDetailsComponent {
readonly inlineExternalFallbackRequested = output<unknown>();
}
@Component({ selector: 'app-stalker-series-view', template: '' })
@Component({
selector: 'app-stalker-series-view',
changeDetection: ChangeDetectionStrategy.Eager,
template: '',
})
class StubStalkerSeriesViewComponent {
readonly vodWithSeries = input<unknown>(null);
readonly providerOnly = input(false);
@@ -6,6 +6,7 @@ import {
inject,
input,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { Location } from '@angular/common';
import { MatSnackBar } from '@angular/material/snack-bar';
@@ -55,6 +56,7 @@ import { createPlaybackSessionKey } from '@iptvnator/playback/util';
selector: 'app-stalker-catalog-detail',
imports: [StalkerSeriesViewComponent, VodDetailsComponent],
templateUrl: './stalker-catalog-detail.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styles: [
`
:host {
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff. Show more