Merge remote-tracking branch 'origin/master' into fix/workspace-route-swap

# Conflicts:
#	apps/web/src/app/app.config.ts
This commit is contained in:
4gray committed 2026-10-13 21:55:08 +02:00
commit 3a68793df2
78 files changed
+3051 -218

No files matched your search

+1
View File
@@ -9,6 +9,7 @@ export * from './lib/confirm-dialog/confirm-dialog.component';
export * from './lib/confirm-dialog/dialog.service';
export * from './lib/content-hero/content-hero.component';
export * from './lib/expandable-text/expandable-text.component';
export * from './lib/form-field-defaults/form-field-defaults';
export * from './lib/external-playback-dock/external-playback-dock.component';
export * from './lib/portal-detail-shell/content-about.component';
export * from './lib/portal-detail-shell/detail-template.directives';
@@ -22,6 +22,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { EpgRuntimeBridgeService, EpgService } from '@iptvnator/epg/data-access';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { debounceTime, from, Subject, switchMap } from 'rxjs';
import { provideFormFieldDefaults } from '../../form-field-defaults/form-field-defaults';
export interface EpgMappingDialogData {
channelKey: string;
@@ -39,6 +40,7 @@ const SEARCH_MIN_CHARS = 2;
@Component({
selector: 'app-epg-mapping-dialog',
providers: [provideFormFieldDefaults()],
templateUrl: './epg-mapping-dialog.component.html',
styleUrl: './epg-mapping-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -0,0 +1,25 @@
import { Provider } from '@angular/core';
import {
MAT_FORM_FIELD_DEFAULT_OPTIONS,
MatFormFieldDefaultOptions,
} from '@angular/material/form-field';
/** The app's outlined, dynamically sized form fields (see m3-theme.scss). */
export const APP_FORM_FIELD_DEFAULTS: MatFormFieldDefaultOptions = {
appearance: 'outline',
subscriptSizing: 'dynamic',
};
/**
* Provide the app's form-field defaults on a component that renders
* `<mat-form-field>`. They are not provided in the root injector: importing
* the token there loads `@angular/material/form-field` (and, since Material
* 22.2, `@angular/forms`) on startup, while every form field lives in a lazy
* chunk. `tools/nx/check-form-field-defaults.mjs` enforces this.
*/
export function provideFormFieldDefaults(): Provider {
return {
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: APP_FORM_FIELD_DEFAULTS,
};
}
@@ -27,6 +27,7 @@ import {
PARENTAL_LOCK_PIN_MAX_LENGTH,
PARENTAL_LOCK_PIN_MIN_LENGTH,
} from '@iptvnator/shared/interfaces';
import { provideFormFieldDefaults } from '../form-field-defaults/form-field-defaults';
export type ParentalLockPinDialogMode = 'unlock' | 'set';
@@ -59,6 +60,7 @@ export interface ParentalLockPinDialogData {
*/
@Component({
selector: 'app-parental-lock-pin-dialog',
providers: [provideFormFieldDefaults()],
templateUrl: './parental-lock-pin-dialog.component.html',
styleUrl: './parental-lock-pin-dialog.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -166,18 +166,20 @@ describe('VodDetailsComponent offline playback', () => {
};
const render = async ({
item = STALKER_VOD,
playbackPosition = null,
externalPlayback = null,
providerOnly = false,
isWatched = false,
}: {
item?: VodDetailsItem;
playbackPosition?: number | null;
externalPlayback?: ExternalPlayerSession | null;
providerOnly?: boolean;
isWatched?: boolean;
} = {}) => {
fixture = TestBed.createComponent(VodDetailsComponent);
fixture.componentRef.setInput('item', STALKER_VOD);
fixture.componentRef.setInput('item', item);
fixture.componentRef.setInput(
'playbackSessionKey',
'stalker-host-owned-key'
@@ -378,6 +380,46 @@ describe('VodDetailsComponent offline playback', () => {
fixture?.destroy();
});
it('renders no provider stand-ins in the hero or the About block', async () => {
await render({
item: createStalkerVodItem(
{
id: '43',
cmd: '/media/file_43.mpg',
info: {
name: 'Placeholder Movie',
o_name: 'N/A',
movie_image: 'N/A',
description: 'N/A',
actors: 'N/A',
director: 'N/A',
releasedate: 'N/A',
genre: '-',
rating_imdb: 'N/A',
rating_kinopoisk: '0',
},
},
'stalker-1'
),
});
// Watch state stamps the same templates into the About block
fixture.componentRef.setInput('inlinePlayback', {
streamUrl: 'https://portal.example/movie.mp4',
title: 'Placeholder Movie',
});
fixture.detectChanges();
const text = fixture.nativeElement.textContent as string;
expect(text).toContain('Placeholder Movie');
expect(text).not.toMatch(/N\/A/i);
expect(text).not.toContain('⭐');
expect(
fixture.nativeElement.querySelectorAll(
'.details__tag:not(.details__tag--clickable), .details__meta-item'
).length
).toBe(0);
});
it('renders Offline and plays a completed download from the primary action', async () => {
completeDownload();
await render();