diff --git a/docs/architecture/zoneless-migration.md b/docs/architecture/zoneless-migration.md
index a6e2ffdde..265a14a6d 100644
--- a/docs/architecture/zoneless-migration.md
+++ b/docs/architecture/zoneless-migration.md
@@ -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 |
diff --git a/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.ts b/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.ts
index 52c4e285d..ea7a5b251 100644
--- a/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.ts
+++ b/libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.ts
@@ -56,7 +56,7 @@ export interface FavoriteLayoutItem {
'./favorites-layout.component.scss',
'../../styles/portal-sidebar.scss',
],
- changeDetection: ChangeDetectionStrategy.Eager,
+ changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CategoryViewComponent,
ContentCardComponent,
diff --git a/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts b/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts
index cae2b8f39..161840ee5 100644
--- a/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts
+++ b/libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts
@@ -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 {
diff --git a/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts b/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts
index 5028d424d..30bffdd1f 100644
--- a/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts
+++ b/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts
@@ -39,7 +39,7 @@ export interface SearchFilter {
TranslatePipe,
],
templateUrl: './search-form.component.html',
- changeDetection: ChangeDetectionStrategy.Eager,
+ changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
.search-container {
diff --git a/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts b/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts
index 2976a0cee..610770e80 100644
--- a/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts
+++ b/libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts
@@ -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 {
diff --git a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts
index 488abdeeb..156cdf6ab 100644
--- a/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts
+++ b/libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts
@@ -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 {
diff --git a/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts b/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts
index efdc2bb9d..5d7ccccc6 100644
--- a/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts
+++ b/libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts
@@ -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 {
diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts
index f38b6b715..5005aa1c1 100644
--- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts
+++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts
@@ -679,6 +679,8 @@ export class StalkerLiveStreamLayoutComponent
);
}
});
+ // OnPush: the Map changed outside any template event.
+ this.cdr.markForCheck();
});
}
diff --git a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts
index a4fd531f4..2d8ab7b36 100644
--- a/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts
+++ b/libs/portal/stalker/feature/src/lib/stalker-series-view/stalker-series-view.component.ts
@@ -155,7 +155,7 @@ interface StalkerSeriesPlaybackRequestContext {
TranslatePipe,
SeasonContainerComponent,
],
- changeDetection: ChangeDetectionStrategy.Eager,
+ changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
StalkerSeriesTmdbSeasonsService,
StalkerSeriesHeroPresenter,
diff --git a/libs/portal/xtream/feature/src/lib/global-search-results/global-search-results.component.ts b/libs/portal/xtream/feature/src/lib/global-search-results/global-search-results.component.ts
index 1b0185345..72d6f17a5 100644
--- a/libs/portal/xtream/feature/src/lib/global-search-results/global-search-results.component.ts
+++ b/libs/portal/xtream/feature/src/lib/global-search-results/global-search-results.component.ts
@@ -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: '',
})
export class GlobalSearchResultsComponent {}
diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.spec.ts b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.spec.ts
index 7afa25423..a05a42348 100644
--- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.spec.ts
+++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.spec.ts
@@ -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();
+ 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}`,
diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts
index a8723a295..623fcb345 100644
--- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts
+++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts
@@ -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());
});
}
diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts
index c29a4e087..c35ead6ed 100644
--- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts
+++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details.component.ts
@@ -87,7 +87,7 @@ import { SerialDetailsDownloadAdapterService } from './serial-details-download-a
SerialDetailsMenuService,
SerialDetailsDownloadAdapterService,
],
- changeDetection: ChangeDetectionStrategy.Eager,
+ changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CastCrewRowComponent,
DetailActionButtonComponent,