Merge master into perf/onpush-libs-playlist

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-06 23:03:41 +02:00
commit 300e6f2cf0
13 files changed
+75 -21

No files matched your search

+12 -12
View File
@@ -149,15 +149,15 @@ picker), so each marks itself on the form's `events` through
### libs/portal (9)
- [ ] `libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.ts`
- [ ] `libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts`
- [ ] `libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts`
- [ ] `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts`
- [ ] `libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts`
- [ ] `libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts`
- [ ] `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts`
- [ ] `libs/portal/xtream/feature/src/lib/global-search-results/global-search-results.component.ts`
- [ ] `libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts`
- [x] `libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.ts`
- [x] `libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts`
- [x] `libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts`
- [x] `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts`
- [x] `libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts`
- [x] `libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts`
- [x] `libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts`
- [x] `libs/portal/xtream/feature/src/lib/global-search-results/global-search-results.component.ts`
- [x] `libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts`
Test-only files that set Eager are not listed; they do not ship. The guard
skips every `*.spec.ts` / `*.test.ts` file with or without a suffix of one
@@ -179,9 +179,9 @@ the field a signal (or a `computed`), or writes it through one.
| [x] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-item/playlist-item.component.ts` `checkPortalStatus` | plain `portalStatus` assigned after `await` in `ngOnInit` (PWA only: skipped when source health is supported) | playlist |
| [x] | `libs/playlist/shared/ui/src/lib/recent-playlists/playlist-info/playlist-info.component.ts` (EPG clear and EPG file pick handlers) | plain `playlist` reassigned after `await` | playlist |
| [x] | `libs/playlist/import/feature/src/lib/stalker-portal-import/stalker-portal-import.component.ts` (device-id derivation) | `form.patchValue` after `await`; no fix needed: while derivation is on `hasManualDeviceIds` is false whatever the IDs are, the controls write their own DOM values, and typing goes through value-accessor listeners that mark the view | playlist |
| [ ] | `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` (favorites load) | `favorites` Map filled in a `subscribe` without `markForCheck`; the component is OnPush already, so this is a latent bug today | portal |
| [ ] | `libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts` (favorites load) | same pattern; the neighbouring `favoriteMarks.changes$` handler does call `markForCheck` | portal |
| [ ] | same file, programme dialog `afterClosed` | deletes from `epgPrograms`/`currentProgramsProgress` after `await` without marking | portal |
| [x] | `libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts` (favorites load) | `favorites` Map filled in a `subscribe` without `markForCheck`; the component is OnPush already, so this is a latent bug today | portal |
| [x] | `libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts` (favorites load) | same pattern; the neighbouring `favoriteMarks.changes$` handler does call `markForCheck` | portal |
| [x] | same file, programme dialog `afterClosed` | deletes from `epgPrograms`/`currentProgramsProgress` after `await` without marking | portal |
| [x] | `apps/web/src/app/settings/settings-backup.facade.ts` (backup import) | `change` listener on a detached file input → `hydrateFromStore()`; section templates read `form().value.theme`/`coverSize`; each section now marks itself on its form's `events` (`markSectionForCheckOnFormEvents`), which `settings.component.form.spec.ts` guards without a forced render | apps/web |
| [x] | `libs/ui/epg/src/lib/epg-guide/epg-guide.component.ts` (jump to now, keyboard focus) | `afterNextRender` registered from CDK/RxJS callbacks; zone.js followed them with a tick, zoneless schedules no render, so the guide opened at midnight. It now marks itself when it registers the hook. Found by `epg-guide.e2e.ts` on the zoneless build | flag |
| [ ] | `libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts` | plain `isLoading`/`error`/`status` written after `await` and from a 2 s `setInterval` | only if `apps/remote-control-web` goes zoneless |
@@ -56,7 +56,7 @@ export interface FavoriteLayoutItem {
'./favorites-layout.component.scss',
'../../styles/portal-sidebar.scss',
],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CategoryViewComponent,
ContentCardComponent,
@@ -25,7 +25,7 @@ import { DialogService } from '@iptvnator/ui/components';
selector: 'app-playlist-error-view',
templateUrl: './playlist-error-view.component.html',
styleUrls: ['./playlist-error-view.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [MatButtonModule, MatIconModule, RouterLink, TranslateModule],
})
export class PlaylistErrorViewComponent {
@@ -39,7 +39,7 @@ export interface SearchFilter {
TranslatePipe,
],
templateUrl: './search-form.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
.search-container {
@@ -22,7 +22,7 @@ import {
selector: 'app-portal-rail-links',
imports: [MatIcon, MatListModule, MatTooltip, RouterLink, RouterLinkActive],
templateUrl: './portal-rail-links.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './portal-rail-links.component.scss',
})
export class PortalRailLinksComponent {
@@ -60,7 +60,7 @@ import { createPlaybackSessionKey } from '@iptvnator/playback/util';
selector: 'app-stalker-catalog-detail',
imports: [StalkerSeriesViewComponent, VodDetailsComponent],
templateUrl: './stalker-catalog-detail.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
:host {
@@ -27,7 +27,7 @@ type FavoriteButtonItem = StalkerSelectedVodItem & {
selector: 'app-favorites-button',
templateUrl: './stalker-favorites-button.component.html',
styleUrls: ['./stalker-favorites-button.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [AsyncPipe, DetailIconButtonComponent, MatIconModule, TranslateModule],
})
export class FavoritesButtonComponent {
@@ -679,6 +679,8 @@ export class StalkerLiveStreamLayoutComponent
);
}
});
// OnPush: the Map changed outside any template event.
this.cdr.markForCheck();
});
}
@@ -155,7 +155,7 @@ interface StalkerSeriesPlaybackRequestContext {
TranslatePipe,
SeasonContainerComponent,
],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
StalkerSeriesTmdbSeasonsService,
StalkerSeriesHeroPresenter,
@@ -6,7 +6,7 @@ import { SearchResultsComponent } from '../search-results/search-results.compone
selector: 'app-global-search-results',
imports: [SearchResultsComponent],
providers: [XtreamStore],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<app-search-results/>',
})
export class GlobalSearchResultsComponent {}
@@ -900,6 +900,56 @@ describe('PortalChannelsListComponent', () => {
).toBe('live:290');
});
// OnPush: favorites resolve asynchronously (IPC or IndexedDB), outside
// any template event, so the heart must follow without a zone tick.
it('renders favorites that load after the first render', async () => {
const favorites$ = new Subject<FavoriteItem[]>();
favoritesService.getFavorites.mockReturnValue(favorites$);
selectedTypeContentLoading.set(false);
selectedChannels.set([{ title: 'Cartoon Network', xtream_id: 50 }]);
currentPlaylist.set({
id: 'playlist-1',
password: 'secret',
serverUrl: 'http://demo.example',
username: 'demo',
});
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const viewport = fixture.componentInstance.viewport();
Object.defineProperty(
viewport?.elementRef.nativeElement,
'clientHeight',
{ configurable: true, value: 520 }
);
viewport?.checkViewportSize();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const row = () =>
fixture.debugElement.query(By.directive(ChannelListItemComponent))
.componentInstance as ChannelListItemComponent;
expect(row().isFavorite()).toBe(false);
// From here the fixture renders on its own: a forced detectChanges()
// would hide a favorites handler that does not mark the view.
fixture.autoDetectChanges();
favorites$.next([
{
content_id: 1,
playlist_id: 'playlist-1',
type: 'live',
title: 'Cartoon Network',
category_id: 7,
xtream_id: 50,
},
] satisfies FavoriteItem[]);
await fixture.whenStable();
await new Promise((resolve) => setTimeout(resolve));
expect(row().isFavorite()).toBe(true);
});
it('scrolls the virtual list to the selected live channel', () => {
const channels = Array.from({ length: 20 }, (_, index) => ({
title: `Channel ${index + 1}`,
@@ -334,6 +334,7 @@ export class PortalChannelsListComponent implements AfterViewInit, OnDestroy {
true
);
});
this.cdr.markForCheck();
});
// A toggle in another list instance (the sidebar and the
// fullscreen channel panel render this component side by side)
@@ -771,6 +772,7 @@ export class PortalChannelsListComponent implements AfterViewInit, OnDestroy {
this.epgQueueService.invalidate(streamId);
this.epgPrograms.delete(streamId);
this.currentProgramsProgress.delete(streamId);
this.cdr.markForCheck();
this.loadEpgForVisibleChannels(this.channelsToPreview());
});
}
@@ -87,7 +87,7 @@ import { SerialDetailsDownloadAdapterService } from './serial-details-download-a
SerialDetailsMenuService,
SerialDetailsDownloadAdapterService,
],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CastCrewRowComponent,
DetailActionButtonComponent,