mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
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:
1 parent
ef3f98d026
commit
e9eca1c386
200 files changed
+3448
-6823
No files matched your search
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
type: internal
|
||||||
|
area: deps
|
||||||
|
---
|
||||||
|
|
||||||
|
The application uses Angular 22 and Nx 23 with updated build and test tooling.
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Resolve Linux runtime toolchain cache key
|
- name: Resolve Linux runtime toolchain cache key
|
||||||
id: linux-runtime-cache-key
|
id: linux-runtime-cache-key
|
||||||
@@ -379,7 +379,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install Linux system dependencies
|
- name: Install Linux system dependencies
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -141,7 +141,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v7
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Refresh pin when it approaches upstream retention
|
- name: Refresh pin when it approaches upstream retention
|
||||||
id: refresh
|
id: refresh
|
||||||
|
|||||||
@@ -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.
|
- 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
|
- Keep `nx` and every official `@nx/*` package on the same exact version; run
|
||||||
`pnpm run deps:nx:validate` after dependency updates.
|
`pnpm run deps:nx:validate` after dependency updates.
|
||||||
- Vite `7.3.6`, resolved through Angular's build tooling, is patched with
|
- Use the Node version in `.nvmrc` for development and CI. Angular 22 requires
|
||||||
bounded transform prefilters and the upstream precise matchers in
|
Node `^22.22.3 || ^24.15.0` and TypeScript `>=6.0 <6.1` in this workspace.
|
||||||
`patches/vite@7.3.6.patch`. Keep the patch until supported Angular tooling
|
- Vite `8.1.5`, resolved through Angular's build tooling, retains upstream
|
||||||
resolves a Vite version containing the fix, and run `pnpm run deps:vite:test`
|
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.
|
after related dependency updates.
|
||||||
- `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in
|
- `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
|
`patches/app-builder-lib@26.15.7.patch` with the upstream backport
|
||||||
@@ -30,11 +32,16 @@ This file provides guidance to coding agents working in this repository.
|
|||||||
must receive the temporary keychain's own password, not the `.p12` import
|
must receive the temporary keychain's own password, not the `.p12` import
|
||||||
password. macOS runner images since `macos-26-arm64` 20260831 verify that
|
password. macOS runner images since `macos-26-arm64` 20260831 verify that
|
||||||
password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user
|
password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user
|
||||||
name or passphrase you entered is not correct`. Keep the patch until
|
name or passphrase you entered is not correct`. Keep the patch until
|
||||||
electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+),
|
electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+),
|
||||||
and run `pnpm run deps:electron-builder:test` after related dependency
|
and run `pnpm run deps:electron-builder:test` after related dependency
|
||||||
updates — the test fails when the patched version no longer matches the
|
updates — the test fails when the patched version no longer matches the
|
||||||
installed one.
|
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.
|
- 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`
|
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
|
across project roots creates no edge and the imported file lands in no task
|
||||||
|
|||||||
@@ -97,10 +97,12 @@ pnpm nx show projects
|
|||||||
- See `docs/architecture/nx-workspace-boundaries.md` for the current Nx tag and alias policy.
|
- 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
|
- Keep `nx` and every official `@nx/*` package on the same exact version; run
|
||||||
`pnpm run deps:nx:validate` after dependency updates.
|
`pnpm run deps:nx:validate` after dependency updates.
|
||||||
- Vite `7.3.6`, resolved through Angular's build tooling, is patched with
|
- Use the Node version in `.nvmrc` for development and CI. Angular 22 requires
|
||||||
bounded transform prefilters and the upstream precise matchers in
|
Node `^22.22.3 || ^24.15.0` and TypeScript `>=6.0 <6.1` in this workspace.
|
||||||
`patches/vite@7.3.6.patch`. Keep the patch until supported Angular tooling
|
- Vite `8.1.5`, resolved through Angular's build tooling, retains upstream
|
||||||
resolves a Vite version containing the fix, and run `pnpm run deps:vite:test`
|
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.
|
after related dependency updates.
|
||||||
- `app-builder-lib` `26.15.7` (electron-builder's macOS signing) is patched in
|
- `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
|
`patches/app-builder-lib@26.15.7.patch` with the upstream backport
|
||||||
@@ -108,11 +110,16 @@ pnpm nx show projects
|
|||||||
must receive the temporary keychain's own password, not the `.p12` import
|
must receive the temporary keychain's own password, not the `.p12` import
|
||||||
password. macOS runner images since `macos-26-arm64` 20260831 verify that
|
password. macOS runner images since `macos-26-arm64` 20260831 verify that
|
||||||
password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user
|
password, and `Build on macos arm64` failed with `SecKeychainUnlock: The user
|
||||||
name or passphrase you entered is not correct`. Keep the patch until
|
name or passphrase you entered is not correct`. Keep the patch until
|
||||||
electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+),
|
electron-builder resolves an `app-builder-lib` containing the fix (26.16.1+),
|
||||||
and run `pnpm run deps:electron-builder:test` after related dependency
|
and run `pnpm run deps:electron-builder:test` after related dependency
|
||||||
updates — the test fails when the patched version no longer matches the
|
updates — the test fails when the patched version no longer matches the
|
||||||
installed one.
|
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.
|
- 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`
|
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
|
across project roots creates no edge and the imported file lands in no task
|
||||||
|
|||||||
@@ -272,11 +272,18 @@ needed.
|
|||||||
|
|
||||||
Requirements:
|
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:
|
1. Clone this repository and install project dependencies:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
$ nvm install
|
||||||
|
$ nvm use
|
||||||
$ corepack enable
|
$ corepack enable
|
||||||
$ pnpm install
|
$ pnpm install
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"moduleResolution": "nodenext",
|
"moduleResolution": "nodenext",
|
||||||
"module": "NodeNext"
|
"module": "NodeNext",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"]
|
"include": ["**/*.ts"]
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
provideBrowserGlobalErrorListeners,
|
provideBrowserGlobalErrorListeners,
|
||||||
provideZoneChangeDetection,
|
provideZoneChangeDetection,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { provideHttpClient } from '@angular/common/http';
|
import { provideHttpClient, withXhr } from '@angular/common/http';
|
||||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
@@ -17,7 +17,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideHttpClient(),
|
provideHttpClient(withXhr()),
|
||||||
importProvidersFrom(
|
importProvidersFrom(
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot({
|
||||||
defaultLanguage: 'en',
|
defaultLanguage: 'en',
|
||||||
|
|||||||
@@ -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 { RemoteControlComponent } from '@iptvnator/ui/remote-control';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
@@ -6,6 +11,8 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
imports: [RemoteControlComponent],
|
imports: [RemoteControlComponent],
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.html',
|
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',
|
styleUrl: './app.scss',
|
||||||
})
|
})
|
||||||
export class App implements OnInit {
|
export class App implements OnInit {
|
||||||
|
|||||||
@@ -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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,5 +10,13 @@
|
|||||||
"src/test-setup.ts",
|
"src/test-setup.ts",
|
||||||
"src/**/*.test.ts",
|
"src/**/*.test.ts",
|
||||||
"src/**/*.spec.ts"
|
"src/**/*.spec.ts"
|
||||||
]
|
],
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"extendedDiagnostics": {
|
||||||
|
"checks": {
|
||||||
|
"nullishCoalescingNotNullable": "suppress",
|
||||||
|
"optionalChainNotNullable": "suppress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"noUncheckedSideEffectImports": false,
|
||||||
|
"types": ["*"],
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
@@ -18,7 +18,26 @@
|
|||||||
"bundle": true,
|
"bundle": true,
|
||||||
"thirdParty": true,
|
"thirdParty": true,
|
||||||
"generatePackageJson": 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": {
|
"serve": {
|
||||||
"continuous": true,
|
"continuous": true,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts"]
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,12 @@ import { expect, test } from './fixtures';
|
|||||||
const fixturePath = join(__dirname, 'fixtures/test.m3u');
|
const fixturePath = join(__dirname, 'fixtures/test.m3u');
|
||||||
|
|
||||||
test('@web @m3u basic playlist import flow', async ({ page }) => {
|
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('/');
|
await page.goto('/');
|
||||||
|
|
||||||
// Basic checks
|
// 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('4 channels')).toBeVisible();
|
||||||
await expect(page.getByText('1. Channel 1')).toBeVisible();
|
await expect(page.getByText('1. Channel 1')).toBeVisible();
|
||||||
await expect(page.getByText('4. HappyKids TV')).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 ({
|
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.goto('/');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Open keyboard shortcuts' }).focus();
|
await page.getByRole('button', { name: 'Open keyboard shortcuts' }).focus();
|
||||||
|
|||||||
@@ -66,6 +66,18 @@ test.skip(
|
|||||||
'DASH ClearKey coverage targets Chromium'
|
'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> {
|
async function serveDashFixtures(page: Page): Promise<void> {
|
||||||
await page.route(`${FIXTURE_HOST}/**`, async (route) => {
|
await page.route(`${FIXTURE_HOST}/**`, async (route) => {
|
||||||
const url = new URL(route.request().url());
|
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 page.getByText('1. ClearKey DASH').click();
|
||||||
await expectVideoPlaying(page);
|
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();
|
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);
|
await expectVideoPlaying(page);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"noImplicitOverride": true,
|
"noImplicitOverride": true,
|
||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -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';
|
import { createEsmPreset } from 'jest-preset-angular/presets/index.js';
|
||||||
|
|
||||||
const angularEsmPreset = createEsmPreset({
|
const angularEsmPreset = createEsmPreset({
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
OnInit,
|
OnInit,
|
||||||
signal,
|
signal,
|
||||||
inject,
|
inject,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
@@ -101,6 +102,8 @@ import { AppUpdateReleaseNotesDialogComponent } from './settings/app-update-rele
|
|||||||
</section>
|
</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: [
|
styles: [
|
||||||
`
|
`
|
||||||
.app-update-notification {
|
.app-update-notification {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
OnInit,
|
OnInit,
|
||||||
signal,
|
signal,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Router, RouterOutlet } from '@angular/router';
|
import { Router, RouterOutlet } from '@angular/router';
|
||||||
@@ -48,6 +49,8 @@ const debugAppComponent = createDevLogger('AppComponent');
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
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: [
|
imports: [
|
||||||
AppStartupStatusComponent,
|
AppStartupStatusComponent,
|
||||||
AppUpdateNotificationPanelComponent,
|
AppUpdateNotificationPanelComponent,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
HttpClient,
|
HttpClient,
|
||||||
provideHttpClient,
|
provideHttpClient,
|
||||||
withInterceptorsFromDi,
|
withInterceptorsFromDi,
|
||||||
|
withXhr,
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import {
|
import {
|
||||||
FullscreenOverlayContainer,
|
FullscreenOverlayContainer,
|
||||||
@@ -114,7 +115,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
// fullscreen channel panel's sort/context menus and the controls'
|
// fullscreen channel panel's sort/context menus and the controls'
|
||||||
// tooltips render while the player is fullscreen.
|
// tooltips render while the player is fullscreen.
|
||||||
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
|
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },
|
||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withXhr(), withInterceptorsFromDi()),
|
||||||
provideStore({
|
provideStore({
|
||||||
router: routerReducer,
|
router: routerReducer,
|
||||||
playlistState: playlistReducer,
|
playlistState: playlistReducer,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
SecurityContext,
|
SecurityContext,
|
||||||
signal,
|
signal,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@@ -76,7 +77,9 @@ function decorateReleaseNotesHtml(html: string): string {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="release-notes-dialog__version">
|
<div class="release-notes-dialog__version">
|
||||||
<strong>{{ notes()?.releaseName || notes()?.tagName }}</strong>
|
<strong>{{
|
||||||
|
notes()?.releaseName || notes()?.tagName
|
||||||
|
}}</strong>
|
||||||
@if (notes()?.publishedAt; as publishedAt) {
|
@if (notes()?.publishedAt; as publishedAt) {
|
||||||
<span>{{ publishedAt | date: 'mediumDate' }}</span>
|
<span>{{ publishedAt | date: 'mediumDate' }}</span>
|
||||||
}
|
}
|
||||||
@@ -120,6 +123,8 @@ function decorateReleaseNotesHtml(html: string): string {
|
|||||||
</button>
|
</button>
|
||||||
</mat-dialog-actions>
|
</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: [
|
styles: [
|
||||||
`
|
`
|
||||||
.release-notes-dialog {
|
.release-notes-dialog {
|
||||||
@@ -210,9 +215,8 @@ function decorateReleaseNotesHtml(html: string): string {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppUpdateReleaseNotesDialogComponent implements OnInit {
|
export class AppUpdateReleaseNotesDialogComponent implements OnInit {
|
||||||
private readonly data = inject<AppUpdateReleaseNotesDialogData>(
|
private readonly data =
|
||||||
MAT_DIALOG_DATA
|
inject<AppUpdateReleaseNotesDialogData>(MAT_DIALOG_DATA);
|
||||||
);
|
|
||||||
private readonly dialogRef = inject(
|
private readonly dialogRef = inject(
|
||||||
MatDialogRef<AppUpdateReleaseNotesDialogComponent>
|
MatDialogRef<AppUpdateReleaseNotesDialogComponent>
|
||||||
);
|
);
|
||||||
@@ -271,7 +275,9 @@ export class AppUpdateReleaseNotesDialogComponent implements OnInit {
|
|||||||
await window.electron.getAppUpdateReleaseNotes(request)
|
await window.electron.getAppUpdateReleaseNotes(request)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.error.set(error instanceof Error ? error.message : String(error));
|
this.error.set(
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@@ -18,6 +19,8 @@ import {
|
|||||||
imports: [MatButtonModule, MatIconModule, TranslateModule],
|
imports: [MatButtonModule, MatIconModule, TranslateModule],
|
||||||
templateUrl: './settings-about-section.component.html',
|
templateUrl: './settings-about-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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: [
|
styles: [
|
||||||
':host { display: contents; }',
|
':host { display: contents; }',
|
||||||
'.version-block .build-commit { opacity: 0.65; font-size: 0.85em; }',
|
'.version-block .build-commit { opacity: 0.65; font-size: 0.85em; }',
|
||||||
@@ -28,7 +31,9 @@ export class SettingsAboutSectionComponent {
|
|||||||
readonly version = input<string | undefined>();
|
readonly version = input<string | undefined>();
|
||||||
readonly buildCommit = input<string | undefined>();
|
readonly buildCommit = input<string | undefined>();
|
||||||
readonly updateMessage = input<string | undefined>();
|
readonly updateMessage = input<string | undefined>();
|
||||||
readonly appUpdateStatus = input<ElectronBridgeAppUpdateStatus | null>(null);
|
readonly appUpdateStatus = input<ElectronBridgeAppUpdateStatus | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
readonly buildCommitShort = computed(() => {
|
readonly buildCommitShort = computed(() => {
|
||||||
const commit = this.buildCommit()?.trim();
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
@@ -14,6 +20,8 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
],
|
],
|
||||||
templateUrl: './settings-backup-section.component.html',
|
templateUrl: './settings-backup-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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; }'],
|
styles: [':host { display: contents; }'],
|
||||||
})
|
})
|
||||||
export class SettingsBackupSectionComponent {
|
export class SettingsBackupSectionComponent {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@@ -16,6 +21,8 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
],
|
],
|
||||||
templateUrl: './settings-dashboard-section.component.html',
|
templateUrl: './settings-dashboard-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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; }'],
|
styles: [':host { display: contents; }'],
|
||||||
})
|
})
|
||||||
export class SettingsDashboardSectionComponent {
|
export class SettingsDashboardSectionComponent {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
import { MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@@ -25,6 +30,8 @@ type SettingsDeleteSummaryItem = {
|
|||||||
selector: 'app-settings-delete-all-playlists-dialog',
|
selector: 'app-settings-delete-all-playlists-dialog',
|
||||||
templateUrl: './settings-delete-all-playlists-dialog.component.html',
|
templateUrl: './settings-delete-all-playlists-dialog.component.html',
|
||||||
styleUrls: ['./settings-delete-all-playlists-dialog.component.scss'],
|
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: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MatButtonModule,
|
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 { FormArray, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
@@ -28,6 +34,8 @@ import { EpgViewModeOption } from './settings.models';
|
|||||||
],
|
],
|
||||||
templateUrl: './settings-epg-section.component.html',
|
templateUrl: './settings-epg-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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; }'],
|
styles: [':host { display: contents; }'],
|
||||||
})
|
})
|
||||||
export class SettingsEpgSectionComponent {
|
export class SettingsEpgSectionComponent {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@@ -27,6 +33,8 @@ import {
|
|||||||
],
|
],
|
||||||
templateUrl: './settings-general-section.component.html',
|
templateUrl: './settings-general-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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; }'],
|
styles: [':host { display: contents; }'],
|
||||||
})
|
})
|
||||||
export class SettingsGeneralSectionComponent {
|
export class SettingsGeneralSectionComponent {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
@@ -30,6 +36,8 @@ import { SettingsPlayerOption } from './settings.models';
|
|||||||
],
|
],
|
||||||
templateUrl: './settings-playback-section.component.html',
|
templateUrl: './settings-playback-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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; }'],
|
styles: [':host { display: contents; }'],
|
||||||
})
|
})
|
||||||
export class SettingsPlaybackSectionComponent {
|
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 { FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
@@ -24,6 +30,8 @@ import { QRCodeComponent } from 'angularx-qrcode';
|
|||||||
],
|
],
|
||||||
templateUrl: './settings-remote-control-section.component.html',
|
templateUrl: './settings-remote-control-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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; }'],
|
styles: [':host { display: contents; }'],
|
||||||
})
|
})
|
||||||
export class SettingsRemoteControlSectionComponent {
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
@@ -15,6 +21,8 @@ import { SettingsPlaylistDeleteSummary } from './settings.models';
|
|||||||
],
|
],
|
||||||
templateUrl: './settings-reset-section.component.html',
|
templateUrl: './settings-reset-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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; }'],
|
styles: [':host { display: contents; }'],
|
||||||
})
|
})
|
||||||
export class SettingsResetSectionComponent {
|
export class SettingsResetSectionComponent {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
input,
|
input,
|
||||||
signal,
|
signal,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@@ -32,6 +33,8 @@ type TmdbKeyTestState = 'idle' | 'testing' | 'success' | 'error';
|
|||||||
],
|
],
|
||||||
templateUrl: './settings-tmdb-section.component.html',
|
templateUrl: './settings-tmdb-section.component.html',
|
||||||
encapsulation: ViewEncapsulation.None,
|
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: [
|
styles: [
|
||||||
`
|
`
|
||||||
app-settings-tmdb-section {
|
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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
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: `
|
template: `
|
||||||
<h2 mat-dialog-title>
|
<h2 mat-dialog-title>
|
||||||
{{ 'SETTINGS.UNSAVED_DIALOG_TITLE' | translate }}
|
{{ 'SETTINGS.UNSAVED_DIALOG_TITLE' | translate }}
|
||||||
|
|||||||
@@ -472,7 +472,11 @@ describe('SettingsComponent form', () => {
|
|||||||
const webFixture = TestBed.createComponent(SettingsComponent);
|
const webFixture = TestBed.createComponent(SettingsComponent);
|
||||||
const webComponent = webFixture.componentInstance;
|
const webComponent = webFixture.componentInstance;
|
||||||
stubSettingsSideEffects(webComponent);
|
stubSettingsSideEffects(webComponent);
|
||||||
|
const loadSettings = jest.spyOn(webComponent.form, 'loadSettings');
|
||||||
webFixture.detectChanges();
|
webFixture.detectChanges();
|
||||||
|
expect(loadSettings).toHaveBeenCalledTimes(1);
|
||||||
|
// Await hydration before editing; Zone cannot track native awaits.
|
||||||
|
await loadSettings.mock.results[0].value;
|
||||||
await webFixture.whenStable();
|
await webFixture.whenStable();
|
||||||
|
|
||||||
webComponent.settingsForm.patchValue({ theme: Theme.DarkTheme });
|
webComponent.settingsForm.patchValue({ theme: Theme.DarkTheme });
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ describe('SettingsComponent', () => {
|
|||||||
const lifecycleFixture = TestBed.createComponent(SettingsComponent);
|
const lifecycleFixture = TestBed.createComponent(SettingsComponent);
|
||||||
const lifecycleComponent = lifecycleFixture.componentInstance;
|
const lifecycleComponent = lifecycleFixture.componentInstance;
|
||||||
stubSettingsSideEffects(lifecycleComponent);
|
stubSettingsSideEffects(lifecycleComponent);
|
||||||
|
const loadSettings = jest.spyOn(
|
||||||
|
lifecycleComponent.form,
|
||||||
|
'loadSettings'
|
||||||
|
);
|
||||||
const appUpdateInit = jest.spyOn(lifecycleComponent.appUpdate, 'init');
|
const appUpdateInit = jest.spyOn(lifecycleComponent.appUpdate, 'init');
|
||||||
const appUpdateDispose = jest.spyOn(
|
const appUpdateDispose = jest.spyOn(
|
||||||
lifecycleComponent.appUpdate,
|
lifecycleComponent.appUpdate,
|
||||||
@@ -89,6 +93,9 @@ describe('SettingsComponent', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
lifecycleFixture.detectChanges();
|
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();
|
await lifecycleFixture.whenStable();
|
||||||
|
|
||||||
expect(appUpdateInit).toHaveBeenCalledTimes(1);
|
expect(appUpdateInit).toHaveBeenCalledTimes(1);
|
||||||
@@ -197,7 +204,10 @@ describe('SettingsComponent', () => {
|
|||||||
|
|
||||||
it('save-and-leave persists before allowing the navigation', async () => {
|
it('save-and-leave persists before allowing the navigation', async () => {
|
||||||
component.settingsForm.markAsDirty();
|
component.settingsForm.markAsDirty();
|
||||||
jest.spyOn(component.epg, 'fetchConfiguredEpg').mockImplementation();
|
jest.spyOn(
|
||||||
|
component.epg,
|
||||||
|
'fetchConfiguredEpg'
|
||||||
|
).mockImplementation();
|
||||||
answerDialogWith('save');
|
answerDialogWith('save');
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { toSignal } from '@angular/core/rxjs-interop';
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
@@ -93,6 +94,8 @@ export const SETTINGS_DEFAULT_SECTION = 'general';
|
|||||||
SettingsResetSectionComponent,
|
SettingsResetSectionComponent,
|
||||||
SettingsTmdbSectionComponent,
|
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: [
|
providers: [
|
||||||
SettingsAppUpdateFacade,
|
SettingsAppUpdateFacade,
|
||||||
SettingsBackupFacade,
|
SettingsBackupFacade,
|
||||||
|
|||||||
@@ -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';
|
import { installDuplicateVideoJsQualityLevelsWarnFilter } from '@iptvnator/shared/testing';
|
||||||
|
|
||||||
Object.defineProperty(globalThis, 'jest', {
|
Object.defineProperty(globalThis, 'jest', {
|
||||||
@@ -25,7 +28,7 @@ if (!Element.prototype.animate) {
|
|||||||
|
|
||||||
installDuplicateVideoJsQualityLevelsWarnFilter();
|
installDuplicateVideoJsQualityLevelsWarnFilter();
|
||||||
|
|
||||||
setupZoneTestEnv({
|
setupZonelessTestEnv({
|
||||||
errorOnUnknownElements: true,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -12,5 +12,13 @@
|
|||||||
"src/**/*.test.ts",
|
"src/**/*.test.ts",
|
||||||
"src/**/*.spec.ts",
|
"src/**/*.spec.ts",
|
||||||
"src/**/*.stub.ts"
|
"src/**/*.stub.ts"
|
||||||
]
|
],
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"extendedDiagnostics": {
|
||||||
|
"checks": {
|
||||||
|
"nullishCoalescingNotNullable": "suppress",
|
||||||
|
"optionalChainNotNullable": "suppress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"useDefineForClassFields": false
|
"useDefineForClassFields": false,
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
"include": [".astro/types.d.ts", "**/*"],
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
"exclude": ["dist"],
|
"exclude": ["dist"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
},
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,10 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"noUncheckedSideEffectImports": false,
|
||||||
|
"types": ["*"],
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node10",
|
"moduleResolution": "node10",
|
||||||
"types": ["jest", "node"],
|
"types": ["jest", "node"],
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"jest.config.ts",
|
"jest.config.ts",
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ RUN apk add --no-cache python3 make g++ git
|
|||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
COPY .npmrc ./
|
COPY .npmrc ./
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
COPY patches ./patches
|
COPY patches ./patches
|
||||||
|
|
||||||
# Node 25 unbundled Corepack, so `corepack enable` cannot be the way pnpm gets
|
# Node 25 unbundled Corepack, so `corepack enable` cannot be the way pnpm gets
|
||||||
|
|||||||
@@ -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
|
migrations exist. Major Nx updates always use this manual workflow and the
|
||||||
resulting PR runs the full CI pipeline.
|
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
|
## Vite Dev-Server Patch
|
||||||
|
|
||||||
Angular's development builder currently resolves Vite `7.3.6`. That release's
|
Angular's development builder resolves Vite `8.1.5`. It contains upstream precise
|
||||||
asset and worker transform prefilters can catastrophically backtrack on a large
|
asset/worker URL matchers, but also uses them as raw-code prefilters. Those
|
||||||
generated chunk containing unrelated `new URL...` expressions while searching
|
prefilters reject valid expressions containing comments before the handlers can
|
||||||
for a valid `new URL(..., import.meta.url)` construct. The Electron development
|
strip the comments and apply the precise matchers.
|
||||||
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.
|
|
||||||
|
|
||||||
`patches/vite@7.3.6.patch` backports Vite's upstream precise-matcher fix from
|
`patches/vite@8.1.5.patch` adds bounded asset/worker prefilters and uses the asset
|
||||||
[vitejs/vite#21800](https://github.com/vitejs/vite/pull/21800). Bounded
|
prefilter for bundled URL rewriting too. The precise handler matchers remain
|
||||||
prefilters keep the request-time scan linear while allowing comment-bearing
|
unchanged. This preserves the earlier protection against expensive scanning of
|
||||||
asset and worker expressions to reach the precise matcher after Vite strips
|
large false-positive chunks and retains comment-bearing expressions. Remove the
|
||||||
comments. Keep the patch while the supported Angular toolchain resolves Vite
|
patch only when the resolved upstream Vite passes the same behavior checks:
|
||||||
`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:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run deps:vite:test
|
pnpm run deps:vite:test
|
||||||
|
|||||||
@@ -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
|
`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
|
is still fullscreen. A fullscreen launch (below) or F11 followed by the
|
||||||
player's `F` → `Esc` makes that path routine on Windows/Linux.
|
player's `F` → `Esc` makes that path routine on Windows/Linux.
|
||||||
|
|
||||||
3. `WINDOW:TOGGLE_FULLSCREEN` toggles OS-level fullscreen (`setFullScreen`)
|
3. `WINDOW:TOGGLE_FULLSCREEN` toggles OS-level fullscreen (`setFullScreen`)
|
||||||
and, like the maximize toggle, reports the requested state and leaves
|
and, like the maximize toggle, reports the requested state and leaves
|
||||||
the `WINDOW:STATE_CHANGED` push authoritative. Because the transition is
|
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
|
implicit `node-gyp rebuild` bypasses those binaries and makes installation
|
||||||
depend on the host compiler toolchain. The old `node-abi` override belonged
|
depend on the host compiler toolchain. The old `node-abi` override belonged
|
||||||
to v12's removed `prebuild-install` path and is no longer required.
|
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
|
2. Local development supports **Node 22.22.3–22.x or 24.15.0–24.x**, declared in
|
||||||
`engines` as `^22.13.0 || >=24.0.0`.
|
`engines` as `^22.22.3 || ^24.15.0`. Use `.nvmrc` (currently 22.23.2) for
|
||||||
The direct `@faker-js/faker` dependency and current lint tooling require
|
development and CI. Angular 22 sets this supported LTS range and requires
|
||||||
that floor. `electron-builder` 26.15.7 also pulls `@electron/rebuild` 4,
|
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
|
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
|
`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`
|
carries the v26 backport that fully extracts the Snap template's `.tar.7z`
|
||||||
payload; 26.15.0–26.15.6 can
|
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
|
3. Dependabot keeps Electron, native database, packaging, EPG parser, and
|
||||||
version-locked Shaka/mpegts updates out of the shared npm minor/patch group.
|
version-locked Shaka/mpegts updates out of the shared npm minor/patch group.
|
||||||
Those dependencies require standalone PRs so their dedicated package,
|
Those dependencies require standalone PRs so their dedicated package,
|
||||||
@@ -457,7 +458,7 @@ Toolchain notes for the Electron 43 baseline:
|
|||||||
Electron command instead of during package `postinstall`, so `electron`
|
Electron command instead of during package `postinstall`, so `electron`
|
||||||
must not remain in pnpm's `onlyBuiltDependencies` allowlist. The first local
|
must not remain in pnpm's `onlyBuiltDependencies` allowlist. The first local
|
||||||
`pnpm run serve:backend` can include a one-time download; use `pnpm exec
|
`pnpm run serve:backend` can include a one-time download; use `pnpm exec
|
||||||
electron --version` to prewarm it before an offline run.
|
electron --version` to prewarm it before an offline run.
|
||||||
|
|
||||||
Known caveats:
|
Known caveats:
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
import { createEsmPreset } from 'jest-preset-angular/presets/index.js';
|
||||||
|
|
||||||
const angularEsmPreset = createEsmPreset({
|
const angularEsmPreset = createEsmPreset({
|
||||||
|
|||||||
@@ -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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
"noPropertyAccessFromIndexSignature": true,
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"files": [],
|
"files": [],
|
||||||
"include": [],
|
"include": [],
|
||||||
|
|||||||
+5
-2
@@ -6,6 +6,7 @@ import {
|
|||||||
signal,
|
signal,
|
||||||
ViewEncapsulation,
|
ViewEncapsulation,
|
||||||
viewChild,
|
viewChild,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import {
|
import {
|
||||||
@@ -70,6 +71,7 @@ const METHOD_BY_CANDIDATE_KIND: Record<
|
|||||||
selector: 'app-add-playlist',
|
selector: 'app-add-playlist',
|
||||||
templateUrl: './add-playlist-dialog.component.html',
|
templateUrl: './add-playlist-dialog.component.html',
|
||||||
styleUrl: './add-playlist-dialog.component.scss',
|
styleUrl: './add-playlist-dialog.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
encapsulation: ViewEncapsulation.None,
|
encapsulation: ViewEncapsulation.None,
|
||||||
})
|
})
|
||||||
export class AddPlaylistDialogComponent {
|
export class AddPlaylistDialogComponent {
|
||||||
@@ -98,8 +100,9 @@ export class AddPlaylistDialogComponent {
|
|||||||
* the prefill is applied by an effect that re-runs once the child's
|
* the prefill is applied by an effect that re-runs once the child's
|
||||||
* viewChild signal resolves, then clears this.
|
* viewChild signal resolves, then clears this.
|
||||||
*/
|
*/
|
||||||
private readonly pendingPrefill =
|
private readonly pendingPrefill = signal<ProviderImportCandidate | null>(
|
||||||
signal<ProviderImportCandidate | null>(null);
|
null
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Survives the auto-detect surface being destroyed by a method switch, so
|
* 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 { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@@ -139,6 +146,7 @@ function maskUrlQueryPasswords(url: string): string {
|
|||||||
selector: 'app-auto-import',
|
selector: 'app-auto-import',
|
||||||
templateUrl: './auto-import.component.html',
|
templateUrl: './auto-import.component.html',
|
||||||
styleUrl: './auto-import.component.scss',
|
styleUrl: './auto-import.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [
|
imports: [
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
@@ -228,10 +236,7 @@ export class AutoImportComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'xtream':
|
case 'xtream':
|
||||||
push(
|
push('HOME.XTREAM_PLAYLIST.SERVER_URL', candidate.serverUrl);
|
||||||
'HOME.XTREAM_PLAYLIST.SERVER_URL',
|
|
||||||
candidate.serverUrl
|
|
||||||
);
|
|
||||||
push('HOME.XTREAM_PLAYLIST.USERNAME', candidate.username);
|
push('HOME.XTREAM_PLAYLIST.USERNAME', candidate.username);
|
||||||
push(
|
push(
|
||||||
'HOME.XTREAM_PLAYLIST.PASSWORD',
|
'HOME.XTREAM_PLAYLIST.PASSWORD',
|
||||||
@@ -239,14 +244,8 @@ export class AutoImportComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 'stalker':
|
case 'stalker':
|
||||||
push(
|
push('HOME.STALKER_PORTAL.SERVER_URL', candidate.portalUrl);
|
||||||
'HOME.STALKER_PORTAL.SERVER_URL',
|
push('HOME.STALKER_PORTAL.MAC_ADDRESS', candidate.macAddress);
|
||||||
candidate.portalUrl
|
|
||||||
);
|
|
||||||
push(
|
|
||||||
'HOME.STALKER_PORTAL.MAC_ADDRESS',
|
|
||||||
candidate.macAddress
|
|
||||||
);
|
|
||||||
push(
|
push(
|
||||||
'HOME.STALKER_PORTAL.SERIAL_NUMBER',
|
'HOME.STALKER_PORTAL.SERIAL_NUMBER',
|
||||||
candidate.serialNumber
|
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 { MatIconModule } from '@angular/material/icon';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PlaylistFileImportService } from '@iptvnator/playlist/shared/util';
|
import { PlaylistFileImportService } from '@iptvnator/playlist/shared/util';
|
||||||
@@ -11,6 +18,7 @@ const KB = 1024;
|
|||||||
imports: [DragDropFileUploadDirective, MatIconModule, TranslatePipe],
|
imports: [DragDropFileUploadDirective, MatIconModule, TranslatePipe],
|
||||||
selector: 'app-file-upload',
|
selector: 'app-file-upload',
|
||||||
templateUrl: './file-upload.component.html',
|
templateUrl: './file-upload.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styleUrls: ['./file-upload.component.scss'],
|
styleUrls: ['./file-upload.component.scss'],
|
||||||
})
|
})
|
||||||
export class FileUploadComponent {
|
export class FileUploadComponent {
|
||||||
|
|||||||
+8
-1
@@ -1,4 +1,10 @@
|
|||||||
import { Component, inject, output, signal } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
output,
|
||||||
|
signal,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
@@ -59,6 +65,7 @@ interface StalkerSettledIdentity {
|
|||||||
],
|
],
|
||||||
selector: 'app-stalker-portal-import',
|
selector: 'app-stalker-portal-import',
|
||||||
templateUrl: './stalker-portal-import.component.html',
|
templateUrl: './stalker-portal-import.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
:host {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Component, EventEmitter, Output } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Output,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
@@ -12,6 +17,7 @@ import { TranslatePipe } from '@ngx-translate/core';
|
|||||||
selector: 'app-text-import',
|
selector: 'app-text-import',
|
||||||
templateUrl: './text-import.component.html',
|
templateUrl: './text-import.component.html',
|
||||||
styleUrls: ['./text-import.component.scss'],
|
styleUrls: ['./text-import.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [MatInputModule, ReactiveFormsModule, TranslatePipe],
|
imports: [MatInputModule, ReactiveFormsModule, TranslatePipe],
|
||||||
})
|
})
|
||||||
export class TextImportComponent {
|
export class TextImportComponent {
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { Component, inject, OnInit } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
inject,
|
||||||
|
OnInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -14,6 +19,7 @@ import { RuntimeCapabilitiesService } from '@iptvnator/services';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-url-upload',
|
selector: 'app-url-upload',
|
||||||
templateUrl: './url-upload.component.html',
|
templateUrl: './url-upload.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [
|
imports: [
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
|
|||||||
+8
-1
@@ -1,4 +1,10 @@
|
|||||||
import { Component, EventEmitter, Output, inject } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Output,
|
||||||
|
inject,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
AbstractControl,
|
AbstractControl,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -47,6 +53,7 @@ function xtreamServerUrlValidator(
|
|||||||
],
|
],
|
||||||
selector: 'app-xtream-code-import',
|
selector: 'app-xtream-code-import',
|
||||||
templateUrl: './xtream-code-import.component.html',
|
templateUrl: './xtream-code-import.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
: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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
output,
|
output,
|
||||||
signal,
|
signal,
|
||||||
untracked,
|
untracked,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
@@ -59,6 +60,7 @@ import { M3uVodMetadataService } from './m3u-vod-metadata.service';
|
|||||||
TranslatePipe,
|
TranslatePipe,
|
||||||
],
|
],
|
||||||
templateUrl: './m3u-vod-detail.component.html',
|
templateUrl: './m3u-vod-detail.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styleUrls: ['./m3u-vod-detail.component.scss'],
|
styleUrls: ['./m3u-vod-detail.component.scss'],
|
||||||
})
|
})
|
||||||
export class M3uVodDetailComponent {
|
export class M3uVodDetailComponent {
|
||||||
|
|||||||
@@ -105,7 +105,9 @@
|
|||||||
@if (activeChannel.radio === 'true') {
|
@if (activeChannel.radio === 'true') {
|
||||||
<app-audio-player
|
<app-audio-player
|
||||||
[url]="activeChannel.url"
|
[url]="activeChannel.url"
|
||||||
[icon]="activeChannel?.tvg?.logo!"
|
[icon]="
|
||||||
|
$safeNavigationMigration(activeChannel?.tvg?.logo)!
|
||||||
|
"
|
||||||
[channelName]="displayChannelName()"
|
[channelName]="displayChannelName()"
|
||||||
[volume]="volume()"
|
[volume]="volume()"
|
||||||
(volumeChange)="onInlineVolumeChange($event)"
|
(volumeChange)="onInlineVolumeChange($event)"
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
signal,
|
signal,
|
||||||
untracked,
|
untracked,
|
||||||
viewChild,
|
viewChild,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
@@ -226,6 +227,7 @@ function isInsideScrollableRegion(
|
|||||||
{ provide: EPG_GUIDE_SOURCE, useExisting: M3uEpgGuideSourceService },
|
{ provide: EPG_GUIDE_SOURCE, useExisting: M3uEpgGuideSourceService },
|
||||||
],
|
],
|
||||||
templateUrl: './video-player.component.html',
|
templateUrl: './video-player.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styleUrl: './video-player.component.scss',
|
styleUrl: './video-player.component.scss',
|
||||||
})
|
})
|
||||||
export class VideoPlayerComponent
|
export class VideoPlayerComponent
|
||||||
|
|||||||
+8
-5
@@ -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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { TranslatePipe } from '@ngx-translate/core';
|
import { TranslatePipe } from '@ngx-translate/core';
|
||||||
import type { PlaylistType } from '../../add-playlist-menu/playlist-type';
|
import type { PlaylistType } from '../../add-playlist-menu/playlist-type';
|
||||||
|
|
||||||
export type EmptyStateType =
|
export type EmptyStateType =
|
||||||
| 'welcome-dashboard'
|
'welcome-dashboard' | 'welcome-sources' | 'no-results' | 'no-data';
|
||||||
| 'welcome-sources'
|
|
||||||
| 'no-results'
|
|
||||||
| 'no-data';
|
|
||||||
|
|
||||||
interface FeatureCard {
|
interface FeatureCard {
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -100,6 +102,7 @@ const SOURCE_CARDS: readonly SourceCard[] = [
|
|||||||
'./empty-state.responsive.scss',
|
'./empty-state.responsive.scss',
|
||||||
'./empty-state.themes.scss',
|
'./empty-state.themes.scss',
|
||||||
],
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [MatButtonModule, MatIcon, TranslatePipe],
|
imports: [MatButtonModule, MatIcon, TranslatePipe],
|
||||||
})
|
})
|
||||||
export class EmptyStateComponent {
|
export class EmptyStateComponent {
|
||||||
|
|||||||
+3
-9
@@ -171,15 +171,9 @@ describe('PlaylistInfoComponent', () => {
|
|||||||
currentLang: 'en',
|
currentLang: 'en',
|
||||||
get: jest.fn((key: string) => of(key)),
|
get: jest.fn((key: string) => of(key)),
|
||||||
instant: jest.fn((key: string) => key),
|
instant: jest.fn((key: string) => key),
|
||||||
onDefaultLangChange: of({
|
onDefaultLangChange: new Subject(),
|
||||||
lang: 'en',
|
onLangChange: new Subject(),
|
||||||
translations: {},
|
onTranslationChange: new Subject(),
|
||||||
}),
|
|
||||||
onLangChange: of({ lang: 'en', translations: {} }),
|
|
||||||
onTranslationChange: of({
|
|
||||||
lang: 'en',
|
|
||||||
translations: {},
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+8
-1
@@ -1,6 +1,12 @@
|
|||||||
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
|
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
import { Component, DestroyRef, inject, signal } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
DestroyRef,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FormControl,
|
FormControl,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
@@ -159,6 +165,7 @@ type DesktopFileSaveBridge = Pick<
|
|||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
providers: [DatePipe],
|
providers: [DatePipe],
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [
|
imports: [
|
||||||
ClipboardModule,
|
ClipboardModule,
|
||||||
MatButton,
|
MatButton,
|
||||||
|
|||||||
+2
@@ -12,6 +12,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
input,
|
input,
|
||||||
output,
|
output,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { toSignal } from '@angular/core/rxjs-interop';
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { MatIconButton } from '@angular/material/button';
|
import { MatIconButton } from '@angular/material/button';
|
||||||
@@ -33,6 +34,7 @@ import { PlaylistMeta } from '@iptvnator/shared/interfaces';
|
|||||||
selector: 'app-playlist-item',
|
selector: 'app-playlist-item',
|
||||||
templateUrl: './playlist-item.component.html',
|
templateUrl: './playlist-item.component.html',
|
||||||
styleUrls: ['./playlist-item.component.scss'],
|
styleUrls: ['./playlist-item.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [
|
imports: [
|
||||||
SourceHealthIndicatorComponent,
|
SourceHealthIndicatorComponent,
|
||||||
DatePipe,
|
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 { DatePipe } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
MAT_DIALOG_DATA,
|
MAT_DIALOG_DATA,
|
||||||
@@ -107,7 +114,9 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
|
|||||||
@if (entry.status === 'ready') {
|
@if (entry.status === 'ready') {
|
||||||
<span>{{
|
<span>{{
|
||||||
'SOURCE_HEALTH.REASON.' +
|
'SOURCE_HEALTH.REASON.' +
|
||||||
entry.health?.reason | translate
|
$safeNavigationMigration(
|
||||||
|
entry.health?.reason
|
||||||
|
) | translate
|
||||||
}}</span>
|
}}</span>
|
||||||
} @else {
|
} @else {
|
||||||
<span>{{
|
<span>{{
|
||||||
@@ -117,8 +126,9 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
|
|||||||
}
|
}
|
||||||
@if (entry.health?.checkedAt) {
|
@if (entry.health?.checkedAt) {
|
||||||
<time>{{
|
<time>{{
|
||||||
|
$safeNavigationMigration(
|
||||||
entry.health?.checkedAt
|
entry.health?.checkedAt
|
||||||
| date: 'shortTime'
|
) | date: 'shortTime'
|
||||||
}}</time>
|
}}</time>
|
||||||
}
|
}
|
||||||
@if (entry.warning) {
|
@if (entry.warning) {
|
||||||
@@ -170,6 +180,7 @@ export interface SourceCleanupDialogData extends SourceCleanupContext {
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</mat-dialog-actions>`,
|
</mat-dialog-actions>`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
:host {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
input,
|
input,
|
||||||
untracked,
|
untracked,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatTooltip } from '@angular/material/tooltip';
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
@@ -28,6 +29,7 @@ import { PlaylistMeta, sourceHealthType } from '@iptvnator/shared/interfaces';
|
|||||||
role="img"
|
role="img"
|
||||||
></span>
|
></span>
|
||||||
}`,
|
}`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
: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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
+10
-1
@@ -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 { NgComponentOutlet } from '@angular/common';
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
@@ -21,6 +27,7 @@ import { CategoryContentViewComponent } from './category-content-view.component'
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-grid-list',
|
selector: 'app-grid-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
class MockGridListComponent {
|
class MockGridListComponent {
|
||||||
@@ -37,6 +44,7 @@ class MockGridListComponent {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-playlist-error-view',
|
selector: 'app-playlist-error-view',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
class MockPlaylistErrorViewComponent {
|
class MockPlaylistErrorViewComponent {
|
||||||
@@ -48,6 +56,7 @@ class MockPlaylistErrorViewComponent {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
class MockDetailComponent {
|
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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
+14
-3
@@ -312,7 +312,10 @@
|
|||||||
{{
|
{{
|
||||||
episodeTitle(
|
episodeTitle(
|
||||||
episode.item,
|
episode.item,
|
||||||
episode.episodeMetadata?.title
|
$safeNavigationMigration(
|
||||||
|
episode.episodeMetadata
|
||||||
|
?.title
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</strong>
|
</strong>
|
||||||
@@ -360,7 +363,10 @@
|
|||||||
' ' +
|
' ' +
|
||||||
episodeTitle(
|
episodeTitle(
|
||||||
episode.item,
|
episode.item,
|
||||||
episode.episodeMetadata?.title
|
$safeNavigationMigration(
|
||||||
|
episode.episodeMetadata
|
||||||
|
?.title
|
||||||
|
)
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
(click)="
|
(click)="
|
||||||
@@ -390,7 +396,10 @@
|
|||||||
': ' +
|
': ' +
|
||||||
episodeTitle(
|
episodeTitle(
|
||||||
episode.item,
|
episode.item,
|
||||||
episode.episodeMetadata?.title
|
$safeNavigationMigration(
|
||||||
|
episode.episodeMetadata
|
||||||
|
?.title
|
||||||
|
)
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -418,9 +427,11 @@
|
|||||||
' ' +
|
' ' +
|
||||||
episodeTitle(
|
episodeTitle(
|
||||||
episode.item,
|
episode.item,
|
||||||
|
$safeNavigationMigration(
|
||||||
episode.episodeMetadata
|
episode.episodeMetadata
|
||||||
?.title
|
?.title
|
||||||
)
|
)
|
||||||
|
)
|
||||||
"
|
"
|
||||||
(click)="
|
(click)="
|
||||||
runFileAction(
|
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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
+8
-1
@@ -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 { MatIconButton } from '@angular/material/button';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import { MatIcon } from '@angular/material/icon';
|
||||||
import { MatTooltip } from '@angular/material/tooltip';
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
@@ -50,6 +56,7 @@ export interface FavoriteLayoutItem {
|
|||||||
'./favorites-layout.component.scss',
|
'./favorites-layout.component.scss',
|
||||||
'../../styles/portal-sidebar.scss',
|
'../../styles/portal-sidebar.scss',
|
||||||
],
|
],
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [
|
imports: [
|
||||||
CategoryViewComponent,
|
CategoryViewComponent,
|
||||||
ContentCardComponent,
|
ContentCardComponent,
|
||||||
|
|||||||
+7
-1
@@ -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 { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatIconModule } from '@angular/material/icon';
|
import { MatIconModule } from '@angular/material/icon';
|
||||||
@@ -14,6 +19,7 @@ import { PlaylistActions } from '@iptvnator/m3u-state';
|
|||||||
selector: 'app-playlist-error-view',
|
selector: 'app-playlist-error-view',
|
||||||
templateUrl: './playlist-error-view.component.html',
|
templateUrl: './playlist-error-view.component.html',
|
||||||
styleUrls: ['./playlist-error-view.component.scss'],
|
styleUrls: ['./playlist-error-view.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule],
|
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule],
|
||||||
})
|
})
|
||||||
export class PlaylistErrorViewComponent {
|
export class PlaylistErrorViewComponent {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
model,
|
model,
|
||||||
Output,
|
Output,
|
||||||
ViewChild,
|
ViewChild,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
@@ -38,6 +39,7 @@ export interface SearchFilter {
|
|||||||
TranslatePipe,
|
TranslatePipe,
|
||||||
],
|
],
|
||||||
templateUrl: './search-form.component.html',
|
templateUrl: './search-form.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
.search-container {
|
.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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { InfiniteScrollDirective } from './infinite-scroll.directive';
|
import { InfiniteScrollDirective } from './infinite-scroll.directive';
|
||||||
@@ -13,6 +13,7 @@ import { InfiniteScrollDirective } from './infinite-scroll.directive';
|
|||||||
[infiniteResetKey]="resetKey()"
|
[infiniteResetKey]="resetKey()"
|
||||||
(infiniteLoadMore)="onLoadMore()"
|
(infiniteLoadMore)="onLoadMore()"
|
||||||
></div>`,
|
></div>`,
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
imports: [InfiniteScrollDirective],
|
imports: [InfiniteScrollDirective],
|
||||||
})
|
})
|
||||||
class TestHostComponent {
|
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 { MatIcon } from '@angular/material/icon';
|
||||||
import { MatListModule } from '@angular/material/list';
|
import { MatListModule } from '@angular/material/list';
|
||||||
import { MatTooltip } from '@angular/material/tooltip';
|
import { MatTooltip } from '@angular/material/tooltip';
|
||||||
@@ -8,12 +13,16 @@ import {
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
RouterLinkActive,
|
RouterLinkActive,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { PortalRailLink, PortalRailSection } from '@iptvnator/portal/shared/util';
|
import {
|
||||||
|
PortalRailLink,
|
||||||
|
PortalRailSection,
|
||||||
|
} from '@iptvnator/portal/shared/util';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-portal-rail-links',
|
selector: 'app-portal-rail-links',
|
||||||
imports: [MatIcon, MatListModule, MatTooltip, RouterLink, RouterLinkActive],
|
imports: [MatIcon, MatListModule, MatTooltip, RouterLink, RouterLinkActive],
|
||||||
templateUrl: './portal-rail-links.component.html',
|
templateUrl: './portal-rail-links.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styleUrl: './portal-rail-links.component.scss',
|
styleUrl: './portal-rail-links.component.scss',
|
||||||
})
|
})
|
||||||
export class PortalRailLinksComponent {
|
export class PortalRailLinksComponent {
|
||||||
@@ -27,7 +36,9 @@ export class PortalRailLinksComponent {
|
|||||||
readonly variant = input<'list' | 'rail'>('list');
|
readonly variant = input<'list' | 'rail'>('list');
|
||||||
|
|
||||||
resolveIcon(link: PortalRailLink): string {
|
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')) {
|
if (normalizedPath.includes('/workspace/sources')) {
|
||||||
return 'playlist_play';
|
return 'playlist_play';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"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,
|
errorOnUnknownElements: true,
|
||||||
errorOnUnknownProperties: true,
|
errorOnUnknownProperties: true,
|
||||||
});
|
});
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"emitDecoratorMetadata": false,
|
"emitDecoratorMetadata": false,
|
||||||
"module": "preserve"
|
"module": "preserve",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"enableI18nLegacyMessageIdFormat": false,
|
"enableI18nLegacyMessageIdFormat": false,
|
||||||
|
|||||||
+17
-3
@@ -1,5 +1,11 @@
|
|||||||
import { Location } from '@angular/common';
|
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 { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
@@ -33,7 +39,11 @@ function deferred<T>() {
|
|||||||
return { promise, reject, resolve };
|
return { promise, reject, resolve };
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({ selector: 'app-vod-details', template: '' })
|
@Component({
|
||||||
|
selector: 'app-vod-details',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
|
template: '',
|
||||||
|
})
|
||||||
class StubVodDetailsComponent {
|
class StubVodDetailsComponent {
|
||||||
readonly playbackSessionKey = input.required<string>();
|
readonly playbackSessionKey = input.required<string>();
|
||||||
readonly item = input.required<unknown>();
|
readonly item = input.required<unknown>();
|
||||||
@@ -53,7 +63,11 @@ class StubVodDetailsComponent {
|
|||||||
readonly inlineExternalFallbackRequested = output<unknown>();
|
readonly inlineExternalFallbackRequested = output<unknown>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({ selector: 'app-stalker-series-view', template: '' })
|
@Component({
|
||||||
|
selector: 'app-stalker-series-view',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
|
template: '',
|
||||||
|
})
|
||||||
class StubStalkerSeriesViewComponent {
|
class StubStalkerSeriesViewComponent {
|
||||||
readonly vodWithSeries = input<unknown>(null);
|
readonly vodWithSeries = input<unknown>(null);
|
||||||
readonly providerOnly = input(false);
|
readonly providerOnly = input(false);
|
||||||
|
|||||||
+2
@@ -6,6 +6,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
input,
|
input,
|
||||||
signal,
|
signal,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
@@ -55,6 +56,7 @@ import { createPlaybackSessionKey } from '@iptvnator/playback/util';
|
|||||||
selector: 'app-stalker-catalog-detail',
|
selector: 'app-stalker-catalog-detail',
|
||||||
imports: [StalkerSeriesViewComponent, VodDetailsComponent],
|
imports: [StalkerSeriesViewComponent, VodDetailsComponent],
|
||||||
templateUrl: './stalker-catalog-detail.component.html',
|
templateUrl: './stalker-catalog-detail.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.Eager,
|
||||||
styles: [
|
styles: [
|
||||||
`
|
`
|
||||||
:host {
|
:host {
|
||||||
|
|||||||
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user