Files
iptvnator/libs/ui/components/src/lib/season-container/episode-info-dialog.component.ts
T
26ca8e2cb0 perf(ui): make the libs/ui Eager components OnPush (#1818)
* docs(performance): inventory the zoneless change-detection migration

Plan item C6 step 2. docs/architecture/zoneless-migration.md lists the 66
production files (67 components) that still set
ChangeDetectionStrategy.Eager, the ten places where a template-read plain
field is written outside an Angular event, the NgZone and
ChangeDetectorRef calls to remove at the flip, and the IPC, player,
observer, timer and dialog paths checked as signal-safe.

On Angular 22 an unset changeDetection already means OnPush, so only the
explicit Eager components re-render on every tick.

zoneless-migration.spec.ts in the performance harness compares the
checklist with the code: a new Eager component, or a converted one left
unticked, fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(ui): make the libs/ui Eager components OnPush

Plan item C6 step 3 for libs/ui/components and libs/ui/epg: eleven
components (twelve with the EPG trust dialog) set
ChangeDetectionStrategy.Eager and were checked on every tick, among them
the always-mounted EPG progress panel the idle audit found re-rendering
on every idle tick. Their template state is already signals, signal
inputs, immutable dialog data or fields written from template events, so
they switch to OnPush without other changes.

The epg-item-description spec mutated dialog data after creation and
marked only the fixture's host view; it now marks the component's own
view, which OnPush requires. The libs/ui playback and remote-control
components stay Eager for their own PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 16:44:40 +02:00

180 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
import { XtreamSerieEpisode } from '@iptvnator/shared/interfaces';
export interface EpisodeInfoDialogData {
/** e.g. "S01E05" */
episodeLabel: string;
title: string;
plot?: string;
duration?: string;
airDate?: string;
thumbnailUrl?: string;
rating?: string | number;
}
export function buildEpisodeInfoDialogData(
episode: XtreamSerieEpisode,
fallbackSeasonKey: string | undefined
): EpisodeInfoDialogData {
const info =
!episode.info || Array.isArray(episode.info) ? undefined : episode.info;
const seasonNumber = Number(episode.season || fallbackSeasonKey || 1);
const episodeNumber = Number(episode.episode_num || 1);
return {
episodeLabel: `S${String(seasonNumber).padStart(2, '0')}E${String(
episodeNumber
).padStart(2, '0')}`,
title: episode.title,
plot: info?.plot || undefined,
duration: info?.duration || undefined,
airDate: info?.releasedate || undefined,
thumbnailUrl: info?.movie_image || undefined,
rating: info?.rating || undefined,
};
}
/** Result returned when the user chooses to play from the dialog. */
export const EPISODE_INFO_PLAY = 'play' as const;
/**
* Lightweight episode details dialog. Cards and list rows clamp the plot to
* keep fixed heights; the ℹ button opens this dialog with the full text.
*/
@Component({
selector: 'app-episode-info-dialog',
standalone: true,
imports: [MatButtonModule, MatDialogModule, MatIconModule, TranslateModule],
template: `
<div class="episode-info">
@if (data.thumbnailUrl) {
<img
class="episode-info__thumb"
[src]="data.thumbnailUrl"
[alt]="data.title"
(error)="thumbError = true"
[style.display]="thumbError ? 'none' : ''"
/>
}
<div class="episode-info__body">
<div class="episode-info__eyebrow">
{{ data.episodeLabel }}
@if (data.duration) {
<span class="episode-info__chip">{{
data.duration
}}</span>
}
@if (data.airDate) {
<span class="episode-info__chip">{{
data.airDate
}}</span>
}
@if (data.rating) {
<span class="episode-info__chip"
>⭐ {{ data.rating }}</span
>
}
</div>
<h2 mat-dialog-title class="episode-info__title">
{{ data.title }}
</h2>
@if (data.plot) {
<p class="episode-info__plot">{{ data.plot }}</p>
}
</div>
</div>
<div mat-dialog-actions align="end">
<button mat-button mat-dialog-close type="button">
{{ 'CLOSE' | translate }}
</button>
<button
mat-flat-button
type="button"
data-testid="episode-info-play"
(click)="play()"
>
<mat-icon>play_arrow</mat-icon>
{{ 'XTREAM.PLAY' | translate }}
</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
:host {
display: block;
max-width: 560px;
}
.episode-info__thumb {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 12px 12px 0 0;
display: block;
}
.episode-info__body {
padding: 16px 24px 0;
}
.episode-info__eyebrow {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
opacity: 0.7;
}
.episode-info__chip {
font-weight: 500;
letter-spacing: normal;
text-transform: none;
}
.episode-info__title {
margin: 6px 0 0;
padding: 0;
font-size: 1.25rem;
line-height: 1.3;
overflow-wrap: anywhere;
&::before {
display: none;
}
}
.episode-info__plot {
margin: 10px 0 0;
font-size: 0.9375rem;
line-height: 1.6;
opacity: 0.85;
max-height: 50vh;
overflow-y: auto;
}
`,
],
})
export class EpisodeInfoDialogComponent {
readonly data: EpisodeInfoDialogData = inject(MAT_DIALOG_DATA);
private readonly dialogRef = inject(
MatDialogRef<EpisodeInfoDialogComponent>
);
thumbError = false;
play(): void {
this.dialogRef.close(EPISODE_INFO_PLAY);
}
}