-
-
Notifications
You must be signed in to change notification settings - Fork 654
chore: update to angular v20 #613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
Tooling & Configpackage.json, tsconfig.json, angular.json |
Bump Angular packages to 20.x and TypeScript to 5.9; change tsconfig.moduleResolution to "bundler"; add schematic type/typeSeparator metadata in angular.json. |
Bootstrapsrc/main.ts |
Replace platformBrowserDynamic() with platformBrowser(); remove Tauri detection and service worker registration. |
Schematicsangular.json |
Add explicit schematic metadata: types for component/directive/service and typeSeparator entries for guard/interceptor/module/pipe/resolver. |
App Rootsrc/app/app.component.html, src/app/app.component.ts |
Template *ngIf → @if; drop ModalWindow import and change modals type to any[]. |
| Template directive migration Multiple templates (examples below) |
Replace Angular structural directives (*ngIf/*ngFor/ng-template) with @if/@for/@else/@defer-style blocks across many templates (home, recent-playlists, header, mpv-player-bar, multi-epg, video-player, web-player-view, settings, navigation-bar, season-container, serial-details, vod-details, etc.). |
| Component imports cleanup Examples: src/app/*/*/*.component.ts (many) |
Remove CommonModule, NgIf, NgFor from standalone component imports where no longer needed; keep remaining pipes/modules. |
Recent Playlistssrc/app/home/recent-playlists/* |
Migrate template to @if/@for; make searchQuery readonly; add ghostElements readonly array; remove NgIf/NgFor imports. |
Homesrc/app/home/home.component.* |
Template *ngIf → @if; remove NgIf from standalone imports. |
Headersrc/app/shared/components/header/* |
Rewrite menu/modal structures to @if/@for; inline menus; remove NgIf import; change .active-sort background color. |
WhatsNew servicesrc/app/services/whats-new.service.ts |
Comment out ModalWindow import; remove explicit return type annotations from getModalsByVersion and getLatestChanges. |
Audio Playersrc/app/player/components/audio-player/* |
Template conditionals → @if; remove NgIf import; mark @Input({ required: true }) url: string; removed a console.log. |
| Player components (minor metadata changes) e.g., art-player, d-player, epg-item-description, web-player-view, loading-overlay, account-info, season-container, serial-details, vod-details |
Removed NgIf/CommonModule/other module imports from standalone imports; templates updated to new block syntax where applicable; added total input signal to LoadingOverlayComponent. |
Multi-EPG & EPG templatessrc/app/player/components/multi-epg/*, .../epg-item-description/* |
Convert nested *ngFor/*ngIf to @for/@if; adjust trackBy logic (stringified key) and remove debug logs. |
MPV Player Barsrc/app/shared/components/mpv-player-bar/mpv-player-bar.component.html |
Replace *ngIf/*ngFor with @if/@for; explicit thumbnail fallback handling. |
Stalker resources & componentssrc/app/stalker/stalker.store.ts, src/app/stalker/* |
Replace rxResource usages: request → params, loader → loader({ params }) or stream where applicable; update IPC payload construction to use params.* and switch loaders to streams in components. |
Xtream UI tweakssrc/app/xtream-tauri/category-content-view/*, xtream-main-container, player-dialog.scss |
pageSizeOptions now conditional on isStalker; PlayerDialog open calls add maxWidth/maxHeight; .link-input width changed to 100%. |
Tauri native (Rust)src-tauri/src/commands/media.rs |
Add non-empty origin handling for VLC invocation by appending --http-origin=<origin> when origin provided. |
| Styles various .scss files |
Minor CSS adjustments: radio logo sizing for small screens; header .active-sort background color; other small formatting changes. |
Sequence Diagram(s)
sequenceDiagram
autonumber
actor User
participant Browser
participant AppModule as Angular App
Note over Browser,AppModule: Simplified bootstrap
User->>Browser: request app
Browser->>AppModule: platformBrowser().bootstrapModule(AppModule)
AppModule-->>Browser: app initialized (no SW registration)
sequenceDiagram
autonumber
participant Component as UI Component
participant Resource as rxResource
participant Service as IPC/Playlist Service
Note over Component,Resource: Resource API refactor (request/loader → params/stream)
Component->>Resource: params(): { ... }
alt stream-based loader
Resource->>Service: stream(params)
Service-->>Resource: Observable<Data>
Resource-->>Component: emits values/status
end
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Poem
Hoppity hop through templates I prance,
Swapping ngs for @ifs in a tidy new dance.
Bundler-bound boots and players set right,
Params whisper streams in the Stalker’s night.
A rabbit-sized stamp: v20—what a delight! 🐇✨
Pre-merge checks and finishing touches
✅ Passed checks (3 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | The title "chore: update to angular v20" is concise, a single short sentence, and accurately summarizes the primary change in the PR (upgrade/migration to Angular v20 and related dependency/tooling updates visible in package.json, angular.json, and tsconfig). It is specific enough for teammates to understand the main intent without listing files or extraneous details. |
| Docstring Coverage | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. |
✨ Finishing touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
chore/update-angular-to-v20
Comment @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 13
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
src/app/stalker/stalker.store.ts (2)
186-186: Null check order bug: response.js is accessed before verifying response.Move patchState inside the response check and harden the error path.
- patchState(store, { totalCount: response.js.total_items }); - - if (response) { + if (response) { + patchState(store, { totalCount: response.js?.total_items ?? 0 }); const newItems = response.js.data.map((item) => ({ ...item, cover: item.screenshot_uri, })); @@ - } else { - throw new Error( - `Error: ${response.message} (Status: ${response.status})` - ); - } + } + else { + throw new Error('STALKER_REQUEST returned no response for content list'); + }Also applies to: 216-220
141-146: Error construction dereferences undefined response.Throw a fixed message (or use optional chaining) when response is falsy.
- } else { - throw new Error( - `Error: ${response.message} (Status: ${response.status})` - ); - } + } else { + throw new Error('STALKER_REQUEST returned no response for categories'); + }Apply the same pattern to the getContentResource error block (lines 217–220).
Also applies to: 217-220
src/app/home/home.component.ts (1)
14-24: Add standalone: true; imports require standalone components.This component declares an imports array but isn’t marked standalone. In Angular, imports on @component are only valid for standalone components. Add standalone: true to avoid a compile error and ensure the listed imports are honored. Also verify that TranslatePipe is available as a standalone import in your ngx-translate version; if not, import TranslateModule instead. (v18.angular.dev)
Apply this diff:
@Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], + standalone: true, imports: [ HeaderComponent, MatProgressBarModule, RecentPlaylistsComponent, TranslatePipe ], })src/app/player/components/audio-player/audio-player.component.ts (1)
107-111: Avoid using ViewChild in ngOnChanges; guard and don’t reassign src imperatively.
@ViewChildmay not be ready for the firstngOnChanges. Also, with[src]bound, you don’t need to set it manually.- ngOnChanges(changes: SimpleChanges): void { - this.audio.nativeElement.src = changes.url.currentValue; - this.audio.nativeElement.load(); - this.play(); - } + ngOnChanges(changes: SimpleChanges): void { + if ('url' in changes && this.audio?.nativeElement) { + this.audio.nativeElement.load(); + this.play(); + } + }src/app/player/components/video-player/video-player.component.html (1)
30-39: Avoid “undefined” in stream URL when epgParams is missing.Concatenation can produce
...undefined. DefaultepgParamsto empty string.- [options]="{ - sources: [ - { - src: - activeChannel?.url + - activeChannel?.epgParams, - type: 'application/x-mpegURL', - }, - ], - }" + [options]="{ + sources: [ + { + src: (activeChannel?.url ?? '') + (activeChannel?.epgParams ?? ''), + type: 'application/x-mpegURL' + } + ] + }"src/app/player/components/multi-epg/multi-epg-container.component.ts (1)
60-68: Unsubscribed input subscription can leak.Use takeUntilDestroyed() for the playlistChannels subscription.
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; ... @Input() set playlistChannels(value: Observable<Channel[]>) { if (value) { - value.subscribe((channels) => { + value.pipe(takeUntilDestroyed()).subscribe((channels) => { this._playlistChannels = channels; this.initializeVisibleChannels(); this.requestPrograms(); }); } }src/app/shared/components/header/header.component.ts (1)
165-173: Rename opedAddPlaylistDialog → openAddPlaylistDialog and update call sites.Rename the method in src/app/shared/components/header/header.component.ts and update the template bindings below so the click handlers point to the new name.
Locations to update:
- src/app/shared/components/header/header.component.ts: lines ~165-173 (method).
- src/app/shared/components/header/header.component.html: lines 192, 200, 208, 217, 227 (click handlers).
Diff (method rename):
- opedAddPlaylistDialog(type: PlaylistType) { + openAddPlaylistDialog(type: PlaylistType) { this.dialog.open<AddPlaylistDialogComponent, { type: PlaylistType }>( AddPlaylistDialogComponent, { width: '600px', data: { type }, } ); }src/app/xtream/vod-details/vod-details.component.ts (1)
40-42: Guard against potential null parent routeAccessing
this.route.parent.snapshotcan throw whenparentis null. Use optional chaining.Apply this diff:
- private portalId = - this.route.snapshot.paramMap.get('id') ?? - this.route.parent.snapshot.params.id; + private portalId = + this.route.snapshot.paramMap.get('id') ?? + this.route.parent?.snapshot?.params?.id;
🧹 Nitpick comments (49)
src/app/xtream-tauri/player-dialog/player-dialog.component.scss (1)
8-12: Guard against overflow with 100% width in dialog inputsBumping to 100% can introduce horizontal scroll in some layouts (borders/padding, flex containers). Add safe constraints so it never exceeds the container.
.link-input { - width: 100%; + width: 100%; + max-width: 100%; + min-width: 0; // avoids flex overflow + box-sizing: border-box; // ensures borders/padding don’t spill padding-top: 5px; @include mat.form-field-density(-5); }src/app/xtream/season-container/season-container.component.html (3)
3-11: Use stable track expression and ensure numeric season ordering
- Tracking by the whole
seasonobject is unstable withkeyvalue(new entries are new object refs). Track byseason.keyto avoid DOM churn.- If season keys are numeric strings (e.g., "10", "2"), default
keyvalueordering is lexicographic. Provide a numeric comparator to prevent misordered seasons.Apply this diff:
- @if (!selectedSeason) { - @for (season of seasons | keyvalue; track season) { + @if (!selectedSeason) { + @for (season of (seasons | keyvalue: seasonNumberCompare); track season.key) {Add in the component TS:
// In season-container.component.ts readonly seasonNumberCompare = (a: {key: string|number}, b: {key: string|number}) => Number(a.key) - Number(b.key);
13-15: Make the “Back to seasons” card accessible
mat-cardisn’t a button. Add semantic/keyboard affordances or switch to a real button to satisfy a11y.- <mat-card class="episode-item" (click)="selectedSeason = undefined"> + <mat-card class="episode-item" role="button" tabindex="0" + (click)="selectedSeason = undefined" + (keydown.enter)="selectedSeason = undefined" + (keydown.space)="selectedSeason = undefined; $event.preventDefault()"> <div class="episode-title">< Back to seasons</div> </mat-card>
16-30: Track episodes by a stable id and harden null‑safety/a11y on image
track episoderisks re-renders; track by a stable key (e.g.,episode.idorepisode_num).- Guard optional paths in
@ifto avoid runtime errors ifinfois missing.- Add
altandloading="lazy"to the image for a11y and perf.- @for (episode of seasons[selectedSeason]; track episode) { + @for (episode of seasons[selectedSeason]; track episode.id ?? episode.episode_num) { <mat-card class="episode-item" (click)="selectEpisode(episode)" > - @if (episode.info.movie_image) { + @if (episode?.info?.movie_image) { <mat-card-content - ><img [src]="episode.info.movie_image" class="episode-cover" - /></mat-card-content> + ><img [src]="episode?.info?.movie_image" + class="episode-cover" + loading="lazy" + [alt]="episode?.title || ('Episode ' + episode?.episode_num)" /></mat-card-content> } <div class="episode-title"> {{ episode.episode_num }}. {{ episode.title }} </div> </mat-card> }src/app/stalker/stalker.store.ts (1)
88-96: Minor: prefer params() returning undefined to idle the resource.Instead of checking currentPlaylist in the loader, compute params as undefined when playlist is missing; loader won’t run and status stays idle. This matches Angular’s resource docs. (angular.dev)
- params: () => ({ - contentType: store.selectedContentType(), - action: StalkerPortalActions.GetCategories, - currentPlaylist: store.currentPlaylist(), - }), - loader: async ({ params }) => { - if (params.currentPlaylist === undefined) return; + params: () => { + const cp = store.currentPlaylist(); + if (!cp) return undefined; + return { + contentType: store.selectedContentType(), + action: StalkerPortalActions.GetCategories, + currentPlaylist: cp, + }; + }, + loader: async ({ params }) => {src/app/xtream-tauri/category-content-view/category-content-view.component.ts (1)
40-40: Namespace the page-size localStorage key to avoid cross-feature bleed.
category-content-view.component.ts sets 'xtream-page-size' (src/app/xtream-tauri/category-content-view/category-content-view.component.ts:58) and xtream.store.ts reads it for limit (src/app/xtream-tauri/xtream.store.ts:50); use distinct keys per API/mode (e.g., 'xtream:{apiId}:page-size' or 'stalker-page-size').src/app/player/components/video-player/video-player.component.scss (1)
1-6: Ensure.main-containerspans full height: Re-addtop: 0or specifyheight: 100%/100vhfor the absolute<mat-drawer-container class="main-container">; without it, the container will size to its content and may collapse.src/app/home/home.component.ts (1)
86-90: Use undefined (or omit) the snack-bar action instead of null.MatSnackBar.open’s second parameter is an optional string. Passing null can trip strict typing; use undefined or omit the arg. (v12.material.angular.io)
- this.snackBar.open(message, null, { + this.snackBar.open(message, undefined, { duration, });src/main.ts (1)
10-14: preserveWhitespaces here has no effect with AOT bootstrap.With platformBrowser().bootstrapModule, preserveWhitespaces in CompilerOptions only affected JIT. Either remove it or configure whitespace at build time (tsconfig/angular.json) if needed.
platformBrowser() - .bootstrapModule(AppModule, { - preserveWhitespaces: false, - }) + .bootstrapModule(AppModule) .catch((err) => console.error(err));src/app/xtream-tauri/account-info/account-info.component.ts (2)
25-139: Localize remaining static strings.Most of the dialog content is static English. Consider piping these through translate for consistency with the Close button.
Example:
-<h2 mat-dialog-title>Account Information</h2> +<h2 mat-dialog-title>{{ 'ACCOUNT_INFO.TITLE' | translate }}</h2>
170-195: Remove debug logs and harden date parsing.Drop console.log calls and parse epoch strings with an explicit radix (or Number) to avoid NaN in edge cases.
- console.log(playlist); + // noop @@ - console.log(this.accountInfo); if (this.accountInfo) { - this.formattedExpDate = new Date( - parseInt(this.accountInfo.user_info.exp_date) * 1000 - ).toLocaleDateString(); - this.formattedCreatedDate = new Date( - parseInt(this.accountInfo.user_info.created_at) * 1000 - ).toLocaleDateString(); + const exp = Number.parseInt(this.accountInfo.user_info.exp_date, 10); + const created = Number.parseInt(this.accountInfo.user_info.created_at, 10); + this.formattedExpDate = new Date(exp * 1000).toLocaleDateString(); + this.formattedCreatedDate = new Date(created * 1000).toLocaleDateString(); }src/app/xtream-tauri/loading-overlay/loading-overlay.component.ts (1)
12-20: Prefer determinate when total > 0 (show 0% instead of indeterminate).Rendering an indeterminate bar when current is 0 can feel jumpy. Gate determinate solely on total() > 0 and compute 0% naturally.
- @if (current() !== 0 && total() !== 0) { + @if (total() > 0) { <mat-progress-bar mode="determinate" [value]="(current() / total()) * 100" /> <p>{{ current() }} / {{ total() }}</p>src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html (3)
1-4: Avoid double async subscriptions; alias once.Use @if (... | async; as processes) and iterate over processes to prevent re-subscribing and re-evaluating the observable. (angular.dev)
-@if ((activeProcesses$ | async)?.length) { - <div class="mpv-player-bar"> - @for (process of activeProcesses$ | async; track process.id) { +@if (activeProcesses$ | async; as processes) { + @if (processes?.length) { + <div class="mpv-player-bar"> + @for (process of processes; track process.id) {
7-15: Move fallback image logic into a method to avoid direct DOM mutation in templates.Inline assignment to $event.target.src is brittle. Delegate to a handler.
- <img + <img [src]="process.thumbnail" [alt]="process.title" class="media-thumbnail" - (error)=" - $event.target.src = - './assets/images/default-poster.png' - " + (error)="onImgError($event)" />Add in the component TS:
onImgError(e: Event) { (e.target as HTMLImageElement).src = './assets/images/default-poster.png'; }
41-47: Localize the button title.For consistency with the rest of the app, consider translating the Close tooltip.
- title="Close" + [title]="'CLOSE' | translate"src/app/xtream/serial-details/serial-details.component.html (2)
12-14: Add alt/decoding/lazy to cover image for a11y and perf.- <img [src]="item.info.cover" /> + <img [src]="item.info.cover" [attr.alt]="item.info.name || 'cover'" decoding="async" loading="lazy" />
41-46: Prefer semantic containers over label elements.
<label>without a control hurts a11y. Use<p>or<div>.Also applies to: 48-51, 54-60
src/app/player/components/audio-player/audio-player.component.ts (1)
137-139: Remove console log or gate behind env.Leftover
console.login production code.- console.log(direction); this.store.dispatch(setAdjacentChannelAsActive({ direction }));src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html (1)
10-16: Don’t render empty language row when lang is missing.Move the
<p data-test="lang">inside the@if (epgProgram.title[0].lang)block.- @if (epgProgram.title[0].lang) { - <div class="subheading-2"> - {{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }} - </div> - } - <p data-test="lang">{{ epgProgram.title[0].lang }}</p> + @if (epgProgram.title[0].lang) { + <div class="subheading-2"> + {{ 'EPG.PROGRAM_DIALOG.LANGUAGE' | translate }} + </div> + <p data-test="lang">{{ epgProgram.title[0].lang }}</p> + }src/app/player/components/video-player/video-player.component.html (2)
72-74: Add alt text to illustrative images.- <img src="./assets/images/custom-player.png" /> + <img src="./assets/images/custom-player.png" alt="External player illustration" />Also applies to: 92-93
21-26: Consider boolean coercion for radio flag.If
radiocan be'true' | 'false' | boolean, coerce once in the controller and use a boolean in the template.src/app/xtream/navigation-bar/navigation-bar.component.html (2)
15-23: Track function stability.Using a function call in
trackcan create unnecessary churn. Prefer a stable key:track item.contentTypeor a unique id.- @for ( - item of contentTypeNavigationItems; track trackByValue($index, - item)) { + @for (item of contentTypeNavigationItems; track item.contentType) {
93-99: Translate the search placeholder.Hardcoded placeholder breaks i18n consistency.
- placeholder="Search by name" + [placeholder]="'CHANNELS.SEARCH_BY_NAME' | translate"package.json (1)
92-103: Align Angular packages between deps and devDeps.Core Angular libs (
@angular/core/common/router/...) are in devDependencies while others are in dependencies. For clarity and to avoid packaging pitfalls (Electron/Tauri), keep Angular runtime libs aligned (same major and group). Consider moving all@angular/*to devDependencies or dependencies consistently.Also applies to: 37-49
src/app/settings/settings.component.html (9)
43-46: Remove stray matLine attributes from buttons.matLine is for list items, not mat-icon-button; drop it to avoid unknown-attr noise.
- <button - matLine + <button mat-icon-button color="accent" ... - <button - mat-icon-button - matLine + <button + mat-icon-button color="accent"Also applies to: 54-57
57-60: Fix tooltip text for “Remove EPG source”.Tooltip reuses REFRESH_EPG; use a remove-specific key.
- [matTooltip]="'SETTINGS.REFRESH_EPG' | translate" + [matTooltip]="'SETTINGS.REMOVE_EPG_SOURCE' | translate"Please confirm the i18n key exists (or provide the correct one).
48-51: Harden disabled condition (trim whitespace).Prevents “Refresh” enabled when input has only spaces.
- [disabled]="epgField.value === ''" + [disabled]="!epgField.value?.trim()"
113-115: Remove unsupported “for” on mat-label.mat-label doesn’t support “for”; rely on form-field semantics or aria-labelledby.
- <mat-label for="mpvPlayerPath">{{ 'SETTINGS.MPV_PLAYER_PATH' | translate }}</mat-label> + <mat-label>{{ 'SETTINGS.MPV_PLAYER_PATH' | translate }}</mat-label> ... - <mat-label for="vlcPlayerPath">{{ 'SETTINGS.VLC_PLAYER_PATH' | translate }}</mat-label> + <mat-label>{{ 'SETTINGS.VLC_PLAYER_PATH' | translate }}</mat-label>Also applies to: 136-138
92-99: Use stable, primitive track expressions.Track by id/value to reduce DOM churn when arrays reallocate.
- @for (player of players; track player) { + @for (player of players; track player.id) { ... - @for (streamFormat of streamFormatEnum | keyvalue; track streamFormat) { + @for (streamFormat of streamFormatEnum | keyvalue; track streamFormat.value) { ... - @for (language of languageEnum | keyvalue; track language) { + @for (language of languageEnum | keyvalue; track language.value) { ... - @for (theme of themeEnum | keyvalue; track theme) { + @for (theme of themeEnum | keyvalue; track theme.value) {Also applies to: 164-170, 190-198, 218-226
42-52: Add aria-labels to icon buttons.Improves a11y for screen readers.
- <button + <button mat-icon-button color="accent" [matTooltip]="'SETTINGS.REFRESH_EPG' | translate" type="button" [disabled]="!epgField.value?.trim()" (click)="refreshEpg(epgUrl.value[i])" + [attr.aria-label]="'SETTINGS.REFRESH_EPG' | translate" > ... - <button + <button mat-icon-button color="accent" - [matTooltip]="'SETTINGS.REFRESH_EPG' | translate" + [matTooltip]="'SETTINGS.REMOVE_EPG_SOURCE' | translate" type="button" (click)="removeEpgSource(i)" + [attr.aria-label]="'SETTINGS.REMOVE_EPG_SOURCE' | translate" >Also applies to: 53-62
157-159: Placeholders reference VIDEO_PLAYER key; use specific keys for each select.Avoids confusing UX/i18n.
- <mat-label>{{ 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate }}</mat-label> + <mat-label>{{ 'SETTINGS.STREAM_FORMAT_PLACEHOLDER' | translate }}</mat-label> ... - <mat-label>{{ 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate }}</mat-label> + <mat-label>{{ 'SETTINGS.LANGUAGE_PLACEHOLDER' | translate }}</mat-label> ... - <mat-label>{{ 'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate }}</mat-label> + <mat-label>{{ 'SETTINGS.THEME_PLACEHOLDER' | translate }}</mat-label>Please confirm these keys exist (or provide the intended ones).
Also applies to: 183-185, 211-213
277-283: Port input should be numeric with sensible bounds.Tighten validation to avoid invalid ports.
- <input - matInput - type="text" + <input + matInput + type="number" + inputmode="numeric" + min="1024" + max="65535" id="remoteControlPort" formControlName="remoteControlPort" />
355-357: Open external link safely in a new tab.Add target and rel to prevent tab-napping.
- <a href="https://github.com/4gray/iptvnator/releases">{{ + <a href="https://github.com/4gray/iptvnator/releases" target="_blank" rel="noopener noreferrer">{{src/app/shared/components/header/header.component.ts (1)
114-117: Avoid bare subscribe; add automatic teardown.Prevents leaks on component destroy.
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; ... - this.sortService.getSortOptions().subscribe((options) => { + this.sortService + .getSortOptions() + .pipe(takeUntilDestroyed()) + .subscribe((options) => { this.currentSortOptions = options; - }); + });src/app/player/components/multi-epg/multi-epg-container.component.ts (2)
213-221: Date parsing robustness.new Date(dateStr) is format/locale-dependent; prefer date-fns parse with an explicit pattern and TZ, with a safe fallback.
// Example: import { parse } from 'date-fns'; // parse '20250101123000 +0000' as 'yyyyMMddHHmmss xx' const safeParse = (s: string) => parse(s, 'yyyyMMddHHmmss xx', new Date());Apply in getCachedDate() and when creating start/stop dates.
138-140: Replace console.log with a logger or guard by env.Noisy logs in production degrade UX.
if (!ngDevMode) { /* skip logs */ } // or use an injected LoggerService with levelsAlso applies to: 167-171, 181-189
src/app/app.component.ts (1)
49-49: Avoid any[] for modals; restore/define a typed shape.Losing types weakens safety; either import the interface (as type-only) or define a local Modal-like type exposed by WhatsNewService.
- modals: any[] = []; + // If ngx-whats-new exports a type: + // import type { ModalWindow } from 'ngx-whats-new'; + // modals: ModalWindow[] = []; + // Otherwise define a minimal local shape: + type ModalItem = { title: string; content: string; [k: string]: unknown }; + modals: ModalItem[] = [];src/app/portals/web-player-view/web-player-view.component.ts (1)
37-40: Type cast on toSignal is too narrow.settings emits asynchronously; allow undefined in the Signal type.
- ) as Signal<Settings>; + ) as Signal<Settings | undefined>;src/app/portals/web-player-view/web-player-view.component.html (1)
3-6: Remove redundant nested @if (player === 'html5').Outer branch already guards html5; inner check is duplicate.
} @else if (player === 'html5') { - @if (player === 'html5') { - <app-html-video-player [channel]="channel" /> - } + <app-html-video-player [channel]="channel" /> }src/app/xtream/vod-details/vod-details.component.ts (1)
51-76: One-off read should complete subscriptionIf
getPortalFavoritesreturns a long-lived stream, this leaks. Take a single emission.Apply this diff:
- this.playlistService - .getPortalFavorites(this.portalId) - .subscribe((favorites) => { + this.playlistService + .getPortalFavorites(this.portalId) + .pipe(take(1)) + .subscribe((favorites) => {Add import:
-import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute } from '@angular/router'; +import { take } from 'rxjs';src/app/services/whats-new.service.ts (3)
2-2: Restore explicit typing with type-only import (keeps bundler-friendly deep-import avoidance)Commenting the type import and dropping return types weakens API guarantees.
Apply this diff:
-/* import { ModalWindow } from 'ngx-whats-new/lib/modal-window.interface'; */ +import type { ModalWindow } from 'ngx-whats-new';If
ngx-whats-newdoes not re-exportModalWindow, define a local minimal type used by your templates instead of deep-importing.
303-305: Add explicit return typeKeep method contract stable.
Apply this diff:
- getModalsByVersion(version: string) { + getModalsByVersion(version: string): ModalWindow[] { return this.modals[version] || []; }
310-314: Add explicit return typeSame reasoning as above.
Apply this diff:
- getLatestChanges() { + getLatestChanges(): ModalWindow[] { const modalsLength = Object.keys(this.modals).length; const lastVersion = Object.keys(this.modals)[modalsLength - 1]; return this.modals[lastVersion]; }src/app/xtream/vod-details/vod-details.component.html (2)
13-18: Add alt text for poster imageImprove a11y and LCP hints.
Apply this diff:
- <img - [src]="item.info?.movie_image" + <img + [src]="item.info?.movie_image" + [attr.alt]="item.info?.name || 'Poster'" (error)=" $event.target.src = './assets/images/default-poster.png' " />
122-122: Deprecated attribute
frameborderis obsolete in HTML5.Apply this diff:
- frameborder="0"src/app/home/recent-playlists/recent-playlists.component.html (1)
4-12: Add an explicit aria-label to the search inputPlaceholder text isn’t a label; improve screen-reader support.
Apply this diff:
<input matInput #searchQuery type="search" spellcheck="false" autocomplete="off" [placeholder]="'HOME.PLAYLISTS.SEARCH_PLAYLISTS' | translate" + [attr.aria-label]="'HOME.PLAYLISTS.SEARCH_PLAYLISTS' | translate" (input)="onSearchQueryUpdate(searchQuery.value)" />src/app/player/components/multi-epg/multi-epg-container.component.html (1)
34-49: Hard-coded English labels in navigation buttonsLocalize these strings for consistency.
Apply this diff:
- <mat-icon>keyboard_arrow_up</mat-icon> Previous channels + <mat-icon>keyboard_arrow_up</mat-icon> {{ 'EPG.PREVIOUS_CHANNELS' | translate }} ... - <mat-icon>keyboard_arrow_down</mat-icon> Next channels + <mat-icon>keyboard_arrow_down</mat-icon> {{ 'EPG.NEXT_CHANNELS' | translate }}And likewise replace
matTooltip="Previous channels"/"Next channels"with translated keys.src/app/shared/components/header/header.component.html (2)
73-75: Consider translating the GitHub labelAll other items use i18n; keep this consistent if a key exists.
Apply this diff (assuming
MENU.GITHUBexists):- <span>GitHub</span> + <span>{{ 'MENU.GITHUB' | translate }}</span>
189-234: Minor naming nit: opedAddPlaylistDialogMethod name has a typo and is used in multiple actions. Consider renaming to
openAddPlaylistDialogand updating call sites.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (44)
angular.json(1 hunks)package.json(1 hunks)src/app/app.component.html(1 hunks)src/app/app.component.ts(1 hunks)src/app/home/home.component.html(1 hunks)src/app/home/home.component.ts(2 hunks)src/app/home/recent-playlists/recent-playlists.component.html(1 hunks)src/app/home/recent-playlists/recent-playlists.component.ts(2 hunks)src/app/player/components/art-player/art-player.component.ts(2 hunks)src/app/player/components/audio-player/audio-player.component.ts(2 hunks)src/app/player/components/d-player/d-player.component.ts(2 hunks)src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html(1 hunks)src/app/player/components/epg-list/epg-item-description/epg-item-description.component.ts(2 hunks)src/app/player/components/multi-epg/multi-epg-container.component.html(1 hunks)src/app/player/components/multi-epg/multi-epg-container.component.ts(1 hunks)src/app/player/components/video-player/video-player.component.html(2 hunks)src/app/player/components/video-player/video-player.component.scss(1 hunks)src/app/portals/web-player-view/web-player-view.component.html(1 hunks)src/app/portals/web-player-view/web-player-view.component.ts(2 hunks)src/app/services/whats-new.service.ts(2 hunks)src/app/settings/settings.component.html(1 hunks)src/app/shared/components/header/header.component.html(1 hunks)src/app/shared/components/header/header.component.scss(1 hunks)src/app/shared/components/header/header.component.ts(1 hunks)src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html(1 hunks)src/app/stalker/recently-viewed/recently-viewed.component.ts(1 hunks)src/app/stalker/stalker-favorites/stalker-favorites.component.ts(1 hunks)src/app/stalker/stalker-search/stalker-search.component.ts(2 hunks)src/app/stalker/stalker.store.ts(6 hunks)src/app/xtream-tauri/account-info/account-info.component.ts(2 hunks)src/app/xtream-tauri/category-content-view/category-content-view.component.ts(1 hunks)src/app/xtream-tauri/loading-overlay/loading-overlay.component.ts(1 hunks)src/app/xtream-tauri/player-dialog/player-dialog.component.scss(2 hunks)src/app/xtream/navigation-bar/navigation-bar.component.html(1 hunks)src/app/xtream/navigation-bar/navigation-bar.component.ts(2 hunks)src/app/xtream/season-container/season-container.component.html(1 hunks)src/app/xtream/season-container/season-container.component.ts(2 hunks)src/app/xtream/serial-details/serial-details.component.html(1 hunks)src/app/xtream/serial-details/serial-details.component.ts(2 hunks)src/app/xtream/vod-details/vod-details.component.html(2 hunks)src/app/xtream/vod-details/vod-details.component.ts(2 hunks)src/app/xtream/xtream-main-container.component.ts(3 hunks)src/main.ts(1 hunks)tsconfig.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/main.ts (4)
src/environments/environment.dev.ts (1)
AppConfig(6-11)src/environments/environment.prod.ts (1)
AppConfig(1-6)src/environments/environment.web.ts (1)
AppConfig(1-6)src/environments/environment.ts (1)
AppConfig(1-6)
src/app/stalker/stalker-search/stalker-search.component.ts (1)
src/app/stalker/stalker-content-types.ts (1)
StalkerContentTypes(3-32)
src/app/stalker/stalker.store.ts (2)
shared/ipc-commands.ts (1)
STALKER_REQUEST(55-55)src/app/stalker/stalker-content-types.ts (1)
StalkerContentTypes(3-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Codacy Static Code Analysis
- GitHub Check: e2e-tests (macos-latest)
🔇 Additional comments (29)
src/app/xtream-tauri/player-dialog/player-dialog.component.scss (1)
22-22: EOF newline/hygiene looks goodNo functional change; keeping a trailing newline is a good practice.
src/app/stalker/stalker.store.ts (1)
83-148: API shape looks correct for Angular 20 resources (params/loader).The migration from request→params and loader({params}) aligns with current Angular docs. No issues on the new API surface. (angular.dev)
Also applies to: 149-156
src/app/stalker/stalker-search/stalker-search.component.ts (1)
91-98: API migration to params/loader is correct for Angular 20 resources.The new shape matches Angular’s resource API. (angular.dev)
src/app/stalker/recently-viewed/recently-viewed.component.ts (1)
47-51: Good migration to rxResource params/stream.This matches the current rxResource API; using params to trigger refresh via refreshTimestamp is idiomatic.
src/app/stalker/stalker-favorites/stalker-favorites.component.ts (1)
47-51: Good migration to rxResource params/stream.API usage looks correct; refreshTimestamp drives re-subscription.
src/app/shared/components/header/header.component.scss (1)
60-62: LGTM!The styling change from translucent black to solid dark gray for the active sort state aligns with Angular 20's modern control flow syntax migration. The updated color ensures better visual contrast and opaque appearance in the new UI.
tsconfig.json (1)
9-9: Appropriate upgrade to bundler module resolution.The change from "node" to "bundler" module resolution aligns with the broader TypeScript 5.9 upgrade and improves compatibility with modern build tools. This configuration supports Angular 20's build optimizations.
src/app/player/components/d-player/d-player.component.ts (1)
15-16: LGTM!The removal of CommonModule import and setting imports to an empty array is consistent with the Angular 20 migration pattern observed across multiple components. Since this component uses an inline template without structural directives, no additional changes are needed.
src/app/player/components/art-player/art-player.component.ts (1)
15-17: LGTM!The removal of CommonModule import aligns with the new control flow syntax that reduces runtime footprint and improves bundle size. This component's inline template doesn't use structural directives, so the change is safe.
src/app/home/home.component.html (1)
7-9: LGTM! Clean migration to new control flow syntax.The migration from
*ngIf="isLoading"to@if (isLoading)follows Angular 20's new control flow syntax. This provides more ergonomic and intuitive syntax that's closer to JavaScript.src/app/xtream/season-container/season-container.component.ts (1)
1-10: Confirmed: template uses Angular 20 control-flow (@if/@for)
src/app/xtream/season-container/season-container.component.html contains @if/@for (lines 3, 4, 16, 21) and no *ngIf/*ngFor.src/app/xtream/navigation-bar/navigation-bar.component.ts (1)
46-58: Confirmed: template updated to Angular 20 control-flow (@if/@for). src/app/xtream/navigation-bar/navigation-bar.component.html uses @if/@for (lines 7,15,49,54,64,76,88 from rg output); no *ngIf/*ngFor were found.src/main.ts (1)
2-2: Good move: platformBrowser over platformBrowserDynamic.platformBrowserDynamic is deprecated in favor of platformBrowser; this aligns with Angular’s guidance. If you still rely on JIT in non-CLI contexts, ensure @angular/compiler is present (unlikely here). (angular.dev)
src/app/xtream-tauri/account-info/account-info.component.ts (1)
140-140: styleUrl usage is fine on Angular 17+.Using styleUrl (singular) is supported since Angular 17; keep as-is. (blog.ninja-squad.com)
Confirm the project is actually building against Angular ≥17 (this PR targets v20), otherwise switch to styleUrls.
src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html (1)
6-10: Confirm the type of epgProgram.title; likely need the first item’s value.If
titleis an array, rendering{{ epgProgram.title }}will print[object Object]. Considertitle[0]?.value(or model’s actual field).[suggest_minor_issue]
src/app/player/components/epg-list/epg-item-description/epg-item-description.component.ts (1)
12-13: Imports tidy-up looks good.Removal of NgIf import aligns with new control flow syntax.
angular.json (1)
21-43: Config LGTM — serviceWorker set to "ngsw-config.json"; local build required to verify.jq returned "ngsw-config.json"; sandbox build failed with "sh: 1: ng: not found". Run locally (npm run build:prod or ng build -c production) and confirm the build succeeds and the SW config is picked up.
package.json (1)
86-104: Verify builder/test toolchain compatibility with Angular 20
- Findings — @angular-builders/jest v17.0.0 confirmed;
npm ciemitted no peerDependency warnings but showed multiple deprecation notices and 23 vulnerabilities;jqfailed to enumerate @angular/* from package.json during verification.- Action — Inspect package.json (lines 86–104) to confirm TypeScript (expect TS 5.9) and ESLint plugin versions, run the full test suite/CI, and if tests or peer warnings appear, upgrade or pin @angular-builders/jest / ESLint / TS to releases known to be compatible with Angular 20.
src/app/shared/components/header/header.component.ts (1)
1-1: LGTM: dropping NgIf import aligns with @if blocks.No issues spotted; AsyncPipe remains available.
src/app/portals/web-player-view/web-player-view.component.ts (1)
25-29: LGTM: imports narrowed to player components.Matches template’s @if branches.
src/app/xtream/serial-details/serial-details.component.ts (1)
21-25: LGTM: removed NgIf from imports.Template should now rely on @if blocks.
src/app/app.component.html (1)
3-9: *LGTM: migrated ngIf to @if for ngx-whats-new.Bindings unchanged; looks correct.
src/app/xtream/vod-details/vod-details.component.ts (2)
24-29: NgIf removal in standalone imports looks correctTemplate has migrated to the new control-flow; dropping NgIf from imports is appropriate.
80-83: ID type for removeFromFavorites
removeFromFavoritesClickedisEventEmitter<number>, but fallback(this.item as any)?.idcould be non-numeric in stalker mode. Please verify types end-to-end.src/app/xtream/xtream-main-container.component.ts (3)
111-111: Trailing comma in importsNo behavioral change; fine.
360-364: Dialog bounds for VOD playerAdding
maxWidth/maxHeightis a good UX guard on large screens.
390-392: Dialog bounds for episode playerSame here—sensible limits.
src/app/home/recent-playlists/recent-playlists.component.ts (2)
2-2: CommonModule structural directives removal is fineTemplate migrated to new control-flow; keeping only AsyncPipe here is correct.
67-70: Readonly BehaviorSubject + skeleton source look goodMarking
searchQueryreadonly prevents accidental reassignment;ghostElementsenables cheap placeholder loops.
src/app/player/components/audio-player/audio-player.component.ts
Outdated
Show resolved
Hide resolved
src/app/player/components/multi-epg/multi-epg-container.component.html
Outdated
Show resolved
Hide resolved
| params: () => ({ | ||
| contentType: store.selectedContentType(), | ||
| category: store.selectedCategoryId(), | ||
| action: StalkerPortalActions.GetOrderedList, | ||
| search: store.searchPhrase(), | ||
| pageIndex: store.page() + 1, | ||
| }), | ||
| loader: async ({ request }) => { | ||
| loader: async ({ params }) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing reactivity/guard for currentPlaylist in getContentResource.
The loader reads currentPlaylist() but it's not part of params; it won’t rerun when playlist changes and may crash when undefined. Include playlist in params and use it in the loader.
- params: () => ({
- contentType: store.selectedContentType(),
- category: store.selectedCategoryId(),
- action: StalkerPortalActions.GetOrderedList,
- search: store.searchPhrase(),
- pageIndex: store.page() + 1,
- }),
+ params: () => {
+ const playlist = store.currentPlaylist();
+ const category = store.selectedCategoryId();
+ if (!playlist || !category) return undefined;
+ return {
+ contentType: store.selectedContentType(),
+ category,
+ action: StalkerPortalActions.GetOrderedList,
+ search: store.searchPhrase(),
+ pageIndex: store.page() + 1,
+ currentPlaylist: playlist,
+ };
+ },
@@
- url: currentPlaylist().portalUrl,
- macAddress: currentPlaylist().macAddress,
+ url: params.currentPlaylist.portalUrl,
+ macAddress: params.currentPlaylist.macAddress,Also applies to: 180-183
🤖 Prompt for AI Agents
In src/app/stalker/stalker.store.ts around lines 149-156 (and similarly
180-183), the loader reads currentPlaylist() but that value is not included in
the params so the loader won’t rerun when the playlist changes and may access
undefined; add currentPlaylist (or currentPlaylistId) to the params function so
the loader is reactive to playlist changes, and update the loader to guard
against undefined (e.g., return early or throw a controlled error if
params.playlist is missing) before using the playlist value.
| params: () => ({ | ||
| itemId: store.selectedSerialId(), | ||
| }), | ||
| loader: async ({ request }) => { | ||
| loader: async ({ params }) => { | ||
| const { portalUrl, macAddress } = store.currentPlaylist(); | ||
| const params = { | ||
| const queryParams = { | ||
| action: StalkerContentTypes.series.getContentAction, | ||
| type: 'series', | ||
| movie_id: request.itemId, | ||
| movie_id: params.itemId, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
serialSeasonsResource: add playlist to params and guard response.
Ensures reactivity to playlist changes and avoids NPEs.
- params: () => ({
- itemId: store.selectedSerialId(),
- }),
- loader: async ({ params }) => {
- const { portalUrl, macAddress } = store.currentPlaylist();
+ params: () => {
+ const playlist = store.currentPlaylist();
+ const itemId = store.selectedSerialId();
+ if (!playlist || !itemId) return undefined;
+ return { itemId, currentPlaylist: playlist };
+ },
+ loader: async ({ params }) => {
+ const { portalUrl, macAddress } = params.currentPlaylist;
const queryParams = {
action: StalkerContentTypes.series.getContentAction,
type: 'series',
- movie_id: params.itemId,
+ movie_id: params.itemId,
};
const response = await dataService.sendIpcEvent(
STALKER_REQUEST,
{
url: portalUrl,
macAddress,
params: queryParams,
}
);
- return sortByNumericValue(response.js.data);
+ if (!response) throw new Error('STALKER_REQUEST returned no response for seasons');
+ return sortByNumericValue(response.js?.data ?? []);Also applies to: 237-243
🤖 Prompt for AI Agents
In src/app/stalker/stalker.store.ts around lines 224-233 (and also apply same
fix to 237-243), the serialSeasonsResource params only include itemId and the
loader assumes currentPlaylist exists; update params to include the whole
currentPlaylist (or at least portalUrl and macAddress) so the resource reacts to
playlist changes, and in the loader guard the response by checking
currentPlaylist is defined before using its properties and validating the
fetched response (e.g., ensure data exists and has expected shape) before
returning — if missing, return a safe default (empty array/object) or throw a
handled error to avoid NPEs.
| <div class="image"> | ||
| @if (item.info.cover) { | ||
| <img [src]="item.info.cover" /> | ||
| } | ||
| </div> | ||
| <div class="details"> | ||
| <h2>{{ item.info.name }}</h2> | ||
| @if (item.info.plot) { | ||
| <label> | ||
| {{ item.info.plot }} | ||
| </label> | ||
| } | ||
| @if (item.info.releaseDate) { | ||
| <label> | ||
| <div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div> | ||
| {{ item.info.releaseDate }} | ||
| </label> | ||
| } | ||
| @if (item.info.genre) { | ||
| <label> | ||
| <div class="label">{{ 'XTREAM.GENRE' | translate }}:</div> | ||
| {{ item.info.genre }} | ||
| </label> | ||
| } | ||
| @if (item.info.rating) { | ||
| <label> | ||
| <div class="label">{{ 'XTREAM.RATING' | translate }}:</div> | ||
| {{ item.info.rating }} | ||
| </label> | ||
| } | ||
| @if (item.info.cast) { | ||
| <div> | ||
| <div class="label">{{ 'XTREAM.CAST' | translate }}:</div> | ||
| {{ item.info.cast }} | ||
| </div> | ||
| } | ||
| @if (item.info.director) { | ||
| <label> | ||
| <div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div> | ||
| {{ item.info.director }} | ||
| </label> | ||
| } | ||
| @if (item.info.episode_run_time) { | ||
| <label> | ||
| <div class="label"> | ||
| {{ 'XTREAM.EPISODE_RUN_TIME' | translate }}: | ||
| </div> | ||
| <label *ngIf="item.info.director"> | ||
| <div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div> | ||
| {{ item.info.director }} | ||
| </label> | ||
| <label *ngIf="item.info.episode_run_time"> | ||
| <div class="label"> | ||
| {{ 'XTREAM.EPISODE_RUN_TIME' | translate }}: | ||
| </div> | ||
| {{ item.info.episode_run_time }} | ||
| </label> | ||
| <div class="action-buttons"> | ||
| <button | ||
| *ngIf="!isFavorite; else removeFromFavoritesButton" | ||
| mat-stroked-button | ||
| color="accent" | ||
| (click)="toggleFavorite()" | ||
| > | ||
| <mat-icon>star_outline</mat-icon> | ||
| {{ 'PORTALS.ADD_TO_FAVORITES' | translate }} | ||
| </button> | ||
| {{ item.info.episode_run_time }} | ||
| </label> | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against undefined item/info before dereferencing.
Multiple expressions use item.info.* without a parent guard. Wrap the image/details/actions in a single @if (item?.info) (or as info) block to prevent runtime errors when item or item.info is null.
Apply:
- <div class="image">
- @if (item.info.cover) {
+ @if (item?.info) {
+ <div class="image">
+ @if (item.info.cover) {
<img [src]="item.info.cover" />
}
</div>
<div class="details">
<h2>{{ item.info.name }}</h2>
...
- </div>
- </div>
+ </div>
+ </div>
+ }Also applies to: 82-84
🤖 Prompt for AI Agents
In src/app/xtream/serial-details/serial-details.component.html around lines 11
to 60 (and also apply the same fix at lines 82-84), the template dereferences
item.info properties directly which can throw if item or item.info is undefined;
wrap the sections that access item.info (the image, details and related actions)
in a single parent guard such as @if (item?.info) { ... } or use @if (item?.info
as info) { } and then replace inner references with info.* so all accesses are
safe and the runtime errors are prevented.
- Removed unnecessary imports from header, account-info, loading-overlay, navigation-bar, season-container, serial-details, and vod-details components. - Updated template syntax to use structural directives more efficiently in mpv-player-bar, account-info, navigation-bar, season-container, serial-details, and vod-details components. - Enhanced readability and maintainability of the code by organizing imports and using Angular's async pipe and structural directives appropriately. - Adjusted TypeScript configuration to use "bundler" for module resolution for better compatibility with modern build tools.
773faee to
cd2dd2e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/app/player/components/art-player/art-player.component.ts (3)
108-114: MKV playback breaks:typemust matchcustomTypekey.
getVideoType()returnsvideo/matroska, butcustomTypekey ismkv. Artplayer uses string match, so MKV won’t trigger the handler.Apply:
@@ private getVideoType(url: string): string { const extension = url.split('.').pop()?.toLowerCase(); switch (extension) { case 'mkv': - return 'video/matroska'; // Changed from 'mkv' + return 'mkv'; case 'm3u8': return 'm3u8'; case 'mp4': return 'mp4'; default: return 'auto'; } }Also applies to: 84-94
84-94: Hls.js instance is leaked; also ensure properthisbinding.
const hls = new Hls()isn’t destroyed on channel changes or destroy; use a field and tear it down. Also switch to arrow function to preserve lexicalthis.@@ -export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { +export class ArtPlayerComponent implements OnInit, OnDestroy, OnChanges { @@ - private player: Artplayer; + private player?: Artplayer; + private hls?: Hls; @@ - ngOnDestroy(): void { - if (this.player) { - this.player.destroy(); - } - } + ngOnDestroy(): void { + this.hls?.destroy(); + this.hls = undefined; + if (this.player) { + this.player.destroy(); + } + } @@ - ngOnChanges(changes: SimpleChanges): void { - if (changes['channel'] && !changes['channel'].firstChange) { - if (this.player) { - this.player.destroy(); - } - this.initPlayer(); - } - } + ngOnChanges(changes: SimpleChanges): void { + if (changes['channel'] && !changes['channel'].firstChange) { + this.hls?.destroy(); + this.hls = undefined; + if (this.player) { + this.player.destroy(); + } + this.initPlayer(); + } + } @@ - customType: { - m3u8: function (video: HTMLVideoElement, url: string) { - if (Hls.isSupported()) { - const hls = new Hls(); - hls.loadSource(url); - hls.attachMedia(video); - } else if ( - video.canPlayType('application/vnd.apple.mpegurl') - ) { - video.src = url; - } - }, + customType: { + m3u8: (video: HTMLVideoElement, url: string) => { + if (Hls.isSupported()) { + this.hls?.destroy(); + this.hls = new Hls(); + this.hls.loadSource(url); + this.hls.attachMedia(video); + } else if (video.canPlayType('application/vnd.apple.mpegurl')) { + video.src = url; + } + },Also applies to: 46-50, 52-59, 38-41
95-103: MKV error handler can cause retry loops.Reassigning
video.srcinonerrorto the same URL can loop. Log once and don’t reassign.- mkv: function (video: HTMLVideoElement, url: string) { - video.src = url; - // Add error handling - video.onerror = () => { - console.error('Error loading MKV file:', video.error); - // Fallback to treating it as a regular video - video.src = url; - }; - }, + mkv: (video: HTMLVideoElement, url: string) => { + video.onerror = () => { + console.error('Error loading MKV file:', video.error); + }; + video.src = url; + },src/app/stalker/stalker.store.ts (2)
96-112: Stale categories after playlist change (cache not keyed by playlist).Early returns reuse old categories even when params.currentPlaylist changes, serving stale data. Either remove the early-return cache or key caches by playlist.
Apply this diff to drop the stale cache:
- switch (params.contentType) { - case 'itv': - if (store.itvCategories().length > 0) { - return store.itvCategories(); - } - break; - case 'vod': - if (store.vodCategories().length > 0) { - return store.vodCategories(); - } - break; - case 'series': - if (store.seriesCategories().length > 0) { - return store.seriesCategories(); - } - break; - } + // Always fetch categories for the current playlist/content type to avoid stale cache.Optional (outside this hunk): reset caches when playlist changes:
@@ setCurrentPlaylist(playlist: PlaylistMeta | undefined) { patchState(store, { currentPlaylist: playlist }); + // Reset per‑type caches to avoid cross‑playlist leakage. + patchState(store, { + vodCategories: [], + seriesCategories: [], + itvCategories: [], + itvChannels: [], + page: 0, + totalCount: 0, + }); },
141-145: Error path dereferences a falsy response.Else branch tries to read response.message/status when response is falsy, causing a new error.
Apply this diff:
- } else { - throw new Error( - `Error: ${response.message} (Status: ${response.status})` - ); - } + } else { + const message = response?.message ?? 'No response'; + const status = response?.status ? ` (Status: ${response.status})` : ''; + throw new Error(`Error: ${message}${status}`); + }src-tauri/src/commands/media.rs (1)
131-143: Usewait()instead ofwait_with_output()to avoid error when stdio isn’t piped.
wait_with_output()requires stdout/stderr to be piped; here they’re not configured, so this can error and skip exit-status handling. Usewait()and check theExitStatus.- let status = child.wait_with_output(); + let status = child.wait(); @@ - if let Ok(output) = status { - if !output.status.success() { - let error_msg = format!("MPV exited with error code: {}", output.status); + if let Ok(exit_status) = status { + if !exit_status.success() { + let error_msg = format!("MPV exited with error code: {}", exit_status); let _ = app_handle_clone.emit("player-error", error_msg); } }
🧹 Nitpick comments (26)
src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html (3)
1-3: Avoid double subscription to activeProcesses$; capture once withasand reuse.Current template subscribes twice (in @if and @for). Capture the stream once and iterate over the local to prevent duplicate subscriptions and microtasks.
Apply this diff:
-@if ((activeProcesses$ | async)?.length) { - <div class="mpv-player-bar"> - @for (process of activeProcesses$ | async; track process.id) { +@if (activeProcesses$ | async; as processes) { + @if (processes?.length) { + <div class="mpv-player-bar"> + @for (process of processes; track process.id) { @@ - } -</div> + } + </div> + } }Also applies to: 50-52
7-15: Make fallback image path robust and enable lazy loading.Use “assets/...” (no leading ./) to avoid route-relative resolution issues; add loading="lazy" for perf.
Apply this diff:
<img [src]="process.thumbnail" - [alt]="process.title" + [attr.alt]="process.title || 'Stream thumbnail'" + loading="lazy" class="media-thumbnail" - (error)=" - $event.target.src = - './assets/images/default-poster.png' - " + (error)="($event.target as HTMLImageElement).src = 'assets/images/default-poster.png'" />
27-40: Clean up commented-out controls or gate behind a feature flag.Long commented blocks tend to linger and confuse. Remove or guard via a flag.
Example minimal cleanup:
- <!-- <button - mat-icon-button - (click)="pauseStream(process.id)" - title="Pause" - > - <mat-icon>pause</mat-icon> - </button> - <button - mat-icon-button - (click)="playStream(process.id)" - title="Play" - > - <mat-icon>play_arrow</mat-icon> - </button> --> + <!-- TODO: Re-enable pause/play when backend supports stream control -->src/app/shared/components/header/header.component.html (4)
11-19: Add aria-labels to icon buttons for a11y parity with tooltipsTooltips don’t serve screen readers; mirror labels via aria.
<button mat-icon-button class="add-playlist-btn" [matMenuTriggerFor]="addPlaylistMenu" [matTooltip]="'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate" + [attr.aria-label]="'HOME.URL_UPLOAD.ADD_PLAYLIST' | translate" data-test-id="add-playlist" > <button mat-icon-button [matMenuTriggerFor]="sortMenu" [matTooltip]="'HOME.SORT_PLAYLISTS' | translate" + [attr.aria-label]="'HOME.SORT_PLAYLISTS' | translate" data-test-id="sort-playlists" > <button mat-icon-button [matMenuTriggerFor]="filterPlaylistMenu" [matTooltip]="'HOME.FILTER_BY_TYPE' | translate" + [attr.aria-label]="'HOME.FILTER_BY_TYPE' | translate" data-test-id="filter-playlist-by-type" >Also applies to: 21-28, 29-36
56-67: Redundant isHome guard inside Home‑only blockThis menu is already under @if (isHome). Remove the inner guard for simplicity.
-@if (isHome) { <button mat-menu-item [attr.aria-label]="'MENU.SETTINGS_ARIA' | translate" (click)="openSettings()" data-test-id="pwa-open-settings" > <mat-icon>settings</mat-icon> <span>{{ 'MENU.SETTINGS' | translate }}</span> </button> <mat-divider></mat-divider> -}
3-3: Add alt text to logo imageImproves accessibility and Lighthouse scores.
-<img src="./assets/icons/icon-tv-256.png" height="100" /> +<img src="./assets/icons/icon-tv-256.png" height="100" alt="IPTVnator logo" />
175-175: Prefer stable trackBy over $indexUsing $index harms DOM reuse on reordering. Track by a stable key (e.g., type.id or type.value).
-@for (type of playlistTypes; track $index) { +@for (type of playlistTypes; track type.id) {If no id exists, add one or track by a unique property.
src/app/home/recent-playlists/recent-playlists.component.html (3)
42-51: Remove unused loop variable; verify track key stability.let last = $last is unused; drop it. Also ensure item._id is unique and stable; otherwise consider a safer track key.
- @for (item of playlists; track item._id; let last = $last) { + @for (item of playlists; track item._id) {
57-74: Rename unused loop variable for clarity.a isn’t used; underscore makes intent explicit.
- @for (a of ghostElements; track $index) { + @for (_ of ghostElements; track $index) {
1-13: Consider debouncing search input to reduce churn on large lists.onSearchQueryUpdate fires every keystroke; if it triggers heavy work, debounce in the component.
src/app/player/components/multi-epg/multi-epg-container.component.html (4)
38-49: Localize “Previous/Next channels” labels for consistency.Other labels use i18n keys. Use translate pipe for these too.
- [disabled]="channelsLowerRange === 0" - matTooltip="Previous channels" + [disabled]="channelsLowerRange === 0" + [matTooltip]="'EPG.PREVIOUS_CHANNELS' | translate" > - <mat-icon>keyboard_arrow_up</mat-icon> Previous channels + <mat-icon>keyboard_arrow_up</mat-icon> {{ 'EPG.PREVIOUS_CHANNELS' | translate }} ... - [disabled]="isLastPage" - matTooltip="Next channels" + [disabled]="isLastPage" + [matTooltip]="'EPG.NEXT_CHANNELS' | translate" > - <mat-icon>keyboard_arrow_down</mat-icon> Next channels + <mat-icon>keyboard_arrow_down</mat-icon> {{ 'EPG.NEXT_CHANNELS' | translate }}Please confirm the keys exist in your translation files.
54-55: Track expression must be stable and unique; also avoid multiple async subscriptions.
- Ensure
$any(item).channelis globally unique and stable across pagination; prefer an immutable id if available (e.g.,item.id).- Using
channels$ | asyncmultiple times creates multiple subscriptions ifchannels$is cold. Consider hoisting once with@let channels = (channels$ | async) ?? [];and iteratechannels.Example:
- @for (item of channels$ | async; track $any(item).channel; let i = $index) { + @let channels = (channels$ | async) ?? []; + @for (item of channels; track item.id ?? $any(item).channel; let i = $index) {
70-76: Lazy-load channel icons.Small perf win and less layout jank.
- <img + <img + loading="lazy" + decoding="async" [src]="item.icon" [alt]="item.name" style="height: 20px; width: 20px; margin-right: 8px;" >
111-121: Prefer stable property for tracking over calling a function each iteration.
track trackByProgram($index, program)recomputes per cycle. If programs have a stable id, track that directly.- @for (program of item.programs; track trackByProgram($index, program)) { + @for (program of item.programs; track program.id ?? program.uid) {If no id exists, keep the function but ensure it’s pure and stable.
src/app/player/components/art-player/art-player.component.ts (3)
61-70: Null-safety and typings: guard against missingchanneland type ElementRef.Avoid NPEs if
channelisn’t yet set; tighten typings.- constructor(private elementRef: ElementRef) {} + constructor(private elementRef: ElementRef<HTMLElement>) {} @@ - private initPlayer(): void { + private initPlayer(): void { + if (!this.channel?.url) { + return; + }Also applies to: 40-41
65-73:isLiveheuristic is brittle.
includes('m3u8')marks all HLS as live (also VOD). Prefer an explicit flag fromChannel(e.g.,channel.isLive) if available.
36-37: Unused input.
showCaptionsisn’t used. Remove or implement before release.package.json (3)
40-41: Align Angular minors to avoid subtle incompatibilities.Bump CDK/Material to match core 20.3.1.
- "@angular/cdk": "20.2.4", - "@angular/material": "20.2.4", + "@angular/cdk": "20.3.1", + "@angular/material": "20.3.1",
38-38: jest builder should be a devDependency.Move @angular-builders/jest under devDependencies; it’s only needed for tooling.
@@ "dependencies": { - "@angular-builders/jest": "17.0.0", @@ "devDependencies": { + "@angular-builders/jest": "17.0.0",
83-83: Loosen zone.js pin to receive patch fixes.Use caret to pick up 0.15.x patches automatically.
- "zone.js": "~0.15.0" + "zone.js": "^0.15.0"src/app/stalker/stalker.store.ts (1)
196-213: Nit: totalLoaded computation relies on immediate signal read.Use explicit math to avoid relying on synchronous signal updates.
See updated computation in the loader diff above (totalLoaded = params.pageIndex === 1 ? newItems.length : store.itvChannels().length + newItems.length).
src/app/xtream/vod-details/vod-details.component.html (4)
13-18: Add alt and loading hints to the poster image.Improve a11y and performance by providing descriptive alt text and lazy loading.
- <img - [src]="item.info?.movie_image" + <img + [src]="item.info?.movie_image" + [attr.alt]="item.info?.name || 'Poster'" + loading="lazy" + decoding="async"
26-29: Don’t use for non-form content.
<label>is for form controls; use semantic containers (<div>/<p>) to avoid a11y/semantics issues.- <label> + <div> {{ item.info?.description }} - </label> + </div> - <label> + <div> <div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div> {{ item.info?.releasedate }} - </label> + </div> - <label> + <div> <div class="label">{{ 'XTREAM.GENRE' | translate }}:</div> {{ item.info?.genre }} - </label> + </div> - <label> + <div> <div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div> {{ item.info?.country }} - </label> + </div> - <label> + <div> <div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div> {{ item.info?.director }} - </label> + </div> - <label> + <div> <div class="label">{{ 'XTREAM.DURATION' | translate }}:</div> {{ item.info?.duration }} - </label> + </div> - <label> + <div> <div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div> {{ item.info?.rating_imdb }} - </label> + </div> - <label> + <div> <div class="label"> {{ 'XTREAM.KINOPOISK_RATING' | translate }}: </div> {{ item.info?.rating_kinopoisk }} - </label> + </div>Also applies to: 31-35, 37-41, 43-47, 55-59, 61-65, 67-71, 73-79
80-89: Replace with layout spacing.Use flex gap instead of a non-breaking space for spacing between buttons.
- <div class="action-buttons"> + <div class="action-buttons" style="display: flex; gap: 8px;"> ... - {{ 'XTREAM.PLAY' | translate }}</button - > + {{ 'XTREAM.PLAY' | translate }}</button>If you prefer CSS, add
display:flex; gap:8px;in the stylesheet for.action-buttonsinstead of inline style.
115-126: Make iframe title descriptive and drop obsolete attributes.Improve a11y;
frameborderis obsolete in HTML5.- title="YouTube video player" - frameborder="0" + [attr.title]="'YouTube trailer — ' + (item.info?.name || '')"src-tauri/src/commands/media.rs (1)
266-276: Avoid logging full command with sensitive URL/headers at info level.URLs and header args can carry tokens. Prefer debug level (or redact query/headers) to reduce accidental leakage in production logs.
- info!("Complete VLC command: {}", command_str); + log::debug!("Complete VLC command: {}", command_str);
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (46)
angular.json(1 hunks)package.json(1 hunks)src-tauri/src/commands/media.rs(1 hunks)src/app/app.component.html(1 hunks)src/app/app.component.ts(1 hunks)src/app/home/home.component.html(1 hunks)src/app/home/home.component.ts(2 hunks)src/app/home/recent-playlists/recent-playlists.component.html(1 hunks)src/app/home/recent-playlists/recent-playlists.component.ts(2 hunks)src/app/player/components/art-player/art-player.component.ts(2 hunks)src/app/player/components/audio-player/audio-player.component.scss(1 hunks)src/app/player/components/audio-player/audio-player.component.ts(1 hunks)src/app/player/components/d-player/d-player.component.ts(2 hunks)src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html(1 hunks)src/app/player/components/epg-list/epg-item-description/epg-item-description.component.ts(2 hunks)src/app/player/components/multi-epg/multi-epg-container.component.html(1 hunks)src/app/player/components/multi-epg/multi-epg-container.component.ts(1 hunks)src/app/player/components/video-player/video-player.component.html(1 hunks)src/app/player/components/video-player/video-player.component.scss(1 hunks)src/app/portals/web-player-view/web-player-view.component.html(1 hunks)src/app/portals/web-player-view/web-player-view.component.ts(2 hunks)src/app/services/whats-new.service.ts(2 hunks)src/app/settings/settings.component.html(1 hunks)src/app/shared/components/header/header.component.html(1 hunks)src/app/shared/components/header/header.component.scss(1 hunks)src/app/shared/components/header/header.component.ts(1 hunks)src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html(1 hunks)src/app/stalker/recently-viewed/recently-viewed.component.ts(1 hunks)src/app/stalker/stalker-favorites/stalker-favorites.component.ts(1 hunks)src/app/stalker/stalker-search/stalker-search.component.ts(1 hunks)src/app/stalker/stalker.store.ts(6 hunks)src/app/xtream-tauri/account-info/account-info.component.ts(2 hunks)src/app/xtream-tauri/category-content-view/category-content-view.component.ts(1 hunks)src/app/xtream-tauri/loading-overlay/loading-overlay.component.ts(2 hunks)src/app/xtream-tauri/player-dialog/player-dialog.component.scss(2 hunks)src/app/xtream/navigation-bar/navigation-bar.component.html(1 hunks)src/app/xtream/navigation-bar/navigation-bar.component.ts(2 hunks)src/app/xtream/season-container/season-container.component.html(1 hunks)src/app/xtream/season-container/season-container.component.ts(2 hunks)src/app/xtream/serial-details/serial-details.component.html(1 hunks)src/app/xtream/serial-details/serial-details.component.ts(2 hunks)src/app/xtream/vod-details/vod-details.component.html(2 hunks)src/app/xtream/vod-details/vod-details.component.ts(2 hunks)src/app/xtream/xtream-main-container.component.ts(3 hunks)src/main.ts(1 hunks)tsconfig.json(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/app/player/components/audio-player/audio-player.component.scss
🚧 Files skipped from review as they are similar to previous changes (34)
- src/app/player/components/multi-epg/multi-epg-container.component.ts
- src/app/xtream-tauri/category-content-view/category-content-view.component.ts
- src/app/xtream/season-container/season-container.component.html
- src/app/home/home.component.ts
- src/app/stalker/stalker-search/stalker-search.component.ts
- src/app/xtream-tauri/loading-overlay/loading-overlay.component.ts
- src/app/player/components/epg-list/epg-item-description/epg-item-description.component.html
- src/app/home/recent-playlists/recent-playlists.component.ts
- src/app/xtream/season-container/season-container.component.ts
- tsconfig.json
- src/app/player/components/epg-list/epg-item-description/epg-item-description.component.ts
- src/app/player/components/audio-player/audio-player.component.ts
- src/app/app.component.ts
- src/app/stalker/stalker-favorites/stalker-favorites.component.ts
- src/app/player/components/d-player/d-player.component.ts
- src/app/player/components/video-player/video-player.component.html
- src/app/xtream/navigation-bar/navigation-bar.component.html
- src/app/xtream/navigation-bar/navigation-bar.component.ts
- src/app/stalker/recently-viewed/recently-viewed.component.ts
- src/app/player/components/video-player/video-player.component.scss
- src/app/xtream-tauri/account-info/account-info.component.ts
- src/app/services/whats-new.service.ts
- src/app/shared/components/header/header.component.scss
- src/app/xtream/xtream-main-container.component.ts
- src/app/portals/web-player-view/web-player-view.component.ts
- angular.json
- src/app/home/home.component.html
- src/app/portals/web-player-view/web-player-view.component.html
- src/app/xtream/vod-details/vod-details.component.ts
- src/app/xtream/serial-details/serial-details.component.ts
- src/app/shared/components/header/header.component.ts
- src/main.ts
- src/app/app.component.html
- src/app/xtream-tauri/player-dialog/player-dialog.component.scss
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/stalker/stalker.store.ts (2)
shared/ipc-commands.ts (1)
STALKER_REQUEST(55-55)src/app/stalker/stalker-content-types.ts (1)
StalkerContentTypes(3-32)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Codacy Static Code Analysis
- GitHub Check: e2e-tests (macos-latest)
🔇 Additional comments (19)
src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html (1)
1-53: Overall: migration to new control flow looks solid.Use of
@if/@forand per-item markup reads well; track expression is appropriate.Please run the component with Angular’s template type-checking at full strictness to ensure the inline
$event.targetcast passes. If it fails, I can provide a tinyappFallbackImgdirective to handle errors cleanly.src/app/shared/components/header/header.component.html (2)
69-75: Broken GitHub URL (extra slash)-(click)="openUrl('https:///github.com/4gray/iptvnator')" +(click)="openUrl('https://github.com/4gray/iptvnator')"
189-234: Incorrect — template matches the defined method 'opedAddPlaylistDialog'src/app/shared/components/header/header.component.ts:165 defines opedAddPlaylistDialog and src/app/shared/components/header/header.component.html calls it (lines ~192–227); no runtime break. If the intended name is openAddPlaylistDialog, rename both TS and HTML.
Likely an incorrect or invalid review comment.
src/app/xtream/serial-details/serial-details.component.html (2)
11-83: Verify that the existing safety issue has been resolved.I noticed from the past review comments that there was an issue with dereferencing
item.info.*without proper null guards. While the current template uses the new Angular@ifsyntax instead of*ngIf, the same safety concern remains: multiple expressions useitem.info.*without ensuringitemoritem.infoexists first.The template still accesses
item.info.cover,item.info.name,item.info.plot, etc., directly without a parent guard around the entire block.Apply this fix to wrap all
item.infoaccess in a single guard:- <div class="image"> - @if (item.info.cover) { - <img [src]="item.info.cover" /> - } - </div> - <div class="details"> - <h2>{{ item.info.name }}</h2> - @if (item.info.plot) { + @if (item?.info) { + <div class="image"> + @if (item.info.cover) { + <img [src]="item.info.cover" /> + } + </div> + <div class="details"> + <h2>{{ item.info.name }}</h2> + @if (item.info.plot) { <label> {{ item.info.plot }} </label> } <!-- ... rest of the details content ... --> <div class="action-buttons"> <!-- ... buttons content ... --> </div> </div> + }
2-10: LGTM! Successful migration to Angular's new control flow syntax.The template has been successfully migrated from Angular's structural directives (*ngIf, *ngFor) to the new built-in control flow syntax (@if, @for) introduced in Angular 17. The new syntax provides better readability and maintains the same functionality while being more familiar to developers coming from other frameworks.
Notable improvements:
- Clean block-style syntax instead of structural directive asterisks
- Consistent use of
@ifblocks for conditional rendering- Proper handling of the
@elseclause for the favorite/unfavorite button logicAlso applies to: 16-83
src/app/settings/settings.component.html (5)
1-12: LGTM! Clean conditional header rendering.The header section properly uses the new
@if/@elseblock syntax to conditionally render either the app-header component or the dialog title. This is a clean migration from the old structural directive approach.
16-77: LGTM! Excellent EPG URL management implementation.The EPG URL section demonstrates proper use of the new
@forcontrol flow with the requiredtrackexpression for optimal performance. The implementation correctly:
- Uses
@for (_ of epgUrl.controls; track _; let i = $index)for iterating over form array controls- Provides proper form validation and error handling
- Includes intuitive UI controls (refresh, remove, add) for managing EPG sources
- Properly scopes the functionality within the
@if (isTauri)condition
78-148: LGTM! Well-structured video player configuration.The video player selection and path configuration sections effectively use the new control flow syntax:
- The
@for (player of players; track player)loop properly implements the required track expression- Conditional sections for MPV and VLC player paths use
@if (settingsForm.value.player === 'mpv')and@if (settingsForm.value.player === 'vlc')respectively- Clean separation of concerns with proper form control bindings
352-359: LGTM! Clean EPG note rendering.The EPG note section correctly uses
@if (isPwa)to conditionally display the note with proper link handling. The template structure is clean and maintains proper semantic HTML.
362-383: LGTM! Well-structured action buttons.The action buttons section is properly organized with:
- Clear separation using
mat-divider- Proper alignment with
align="end"- Appropriate button states and disabled logic
- Clean use of Angular Material icons and translation pipes
src/app/home/recent-playlists/recent-playlists.component.html (1)
14-55: Approve — template bindings verifiedAll bindings used by the template exist and are correctly typed in src/app/home/recent-playlists/recent-playlists.component.ts (playlists$, ghostElements, allPlaylistsLoaded, sidebarMode, openInfoDialog, getGlobalFavorites, getPlaylist, refreshPlaylist, removeClicked, trackByFn). PlaylistMeta._id is present and used consistently. No issues found.
src/app/player/components/multi-epg/multi-epg-container.component.html (1)
10-23: Prev/Next day tooltips now align with actions.The reversal noted earlier is fixed. Good catch.
package.json (1)
39-39: Resolved: previous skew and tslib floor.Good fixes upgrading @angular/animations to v20 and raising tslib to ≥2.6.2.
Also applies to: 140-140
src/app/stalker/stalker.store.ts (4)
88-96: Good: params now include playlist and guard against undefined.This fixes reactivity and avoids NPEs in getCategoryResource.
149-155: Still missing playlist in getContentResource params; loader is non‑reactive and can NPE.This was flagged previously; loader reads store.currentPlaylist() but params omit it, so the resource won’t reload on playlist change and may crash when playlist is undefined.
Apply this diff:
- params: () => ({ - contentType: store.selectedContentType(), - category: store.selectedCategoryId(), - action: StalkerPortalActions.GetOrderedList, - search: store.searchPhrase(), - pageIndex: store.page() + 1, - }), + params: () => { + const playlist = store.currentPlaylist(); + const category = store.selectedCategoryId(); + const contentType = store.selectedContentType(); + if (!playlist || !contentType) return undefined; + return { + contentType, + category, + action: StalkerPortalActions.GetOrderedList, + search: store.searchPhrase(), + pageIndex: store.page() + 1, + currentPlaylist: playlist, + }; + },
224-241: serialSeasonsResource: add playlist to params and guard response.Same issue as before: missing playlist in params and no guards; can be non‑reactive and crash.
Apply this diff:
- params: () => ({ - itemId: store.selectedSerialId(), - }), - loader: async ({ params }) => { - const { portalUrl, macAddress } = store.currentPlaylist(); + params: () => { + const playlist = store.currentPlaylist(); + const itemId = store.selectedSerialId(); + if (!playlist || !itemId) return undefined; + return { itemId, currentPlaylist: playlist }; + }, + loader: async ({ params }) => { + if (!params?.currentPlaylist) return undefined; + const { portalUrl, macAddress } = params.currentPlaylist; const queryParams = { action: StalkerContentTypes.series.getContentAction, type: 'series', movie_id: params.itemId, }; const response = await dataService.sendIpcEvent( STALKER_REQUEST, { url: portalUrl, macAddress, params: queryParams, } ); - return sortByNumericValue(response.js.data); + if (!response) return []; + return sortByNumericValue(response.js?.data ?? []);
88-147: Verification inconclusive — confirm no loaders read store.currentPlaylist() directlySearch returned no matches; cannot confirm. Verify that:
- All resource loaders read params.currentPlaylist (not store.currentPlaylist).
- All params() functions return undefined when required inputs are missing.
src/app/xtream/vod-details/vod-details.component.html (2)
118-121: Confirmsafepipe returnsSafeResourceUrl.No implementation for the
safepipe was found in the repo (search returned no files). Confirm the pipe calls DomSanitizer.bypassSecurityTrustResourceUrl(...) and returns SafeResourceUrl to avoid security warnings.Location: src/app/xtream/vod-details/vod-details.component.html lines 118–121
81-107: Verify Material/Translate imports for this template.Both standalone components import Material + ngx-translate but use mixed symbols — confirm your Angular/Material version and standardize imports:
- src/app/xtream/vod-details/vod-details.component.ts — imports: MatButtonModule, MatIcon, TranslatePipe.
- src/app/xtream-tauri/vod-details/vod-details.component.ts — imports: MatButton, MatIcon, TranslateModule.
Action: if using an Angular Material release that does not export MatButton/MatIcon as standalone directives, replace MatButton/MatIcon with MatButtonModule and MatIconModule; for ngx-translate prefer TranslatePipe/TranslateDirective in standalone components or TranslateModule for NgModule-based usage.
| "tauri": "tauri" | ||
| }, | ||
| "dependencies": { | ||
| "@angular-builders/jest": "17.0.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify Jest stack compatibility with Angular 20/TS 5.9.
Ensure @angular-builders/jest 17 + jest-preset-angular 14.5.3 work with Angular 20/TypeScript 5.9; you may need newer major versions.
Run:
Optionally confirm via web:
Also applies to: 129-131
🏁 Script executed:
#!/bin/bash
# Check declared versions
jq -r '.dependencies["@angular-builders/jest"], .devDependencies["jest-preset-angular"], .devDependencies.typescript' package.json
# Grep for test builder usage
rg -nC2 '"test"\s*:\s*\{[^\}]*"builder"|@"builder"' -g angular.json || rg -nC2 '"test"' angular.json
# Print peer deps hints (no install)
node -e 'console.log("Check release notes for jest-preset-angular vs TS 5.9 / NG 20");'Length of output: 263
🌐 Web query:
Is jest-preset-angular 14.x compatible with Angular 20 and TypeScript 5.9?
💡 Result:
Short answer: No — jest-preset-angular 14.x is not officially compatible with Angular 20. Its published peerDependencies cap Angular versions below 20, and while its TypeScript peer is permissive (>=4.8) that does not guarantee full, tested compatibility with TypeScript 5.9. See sources:
- package/compatibility listings and changelog showing v14 peer range targeting Angular <18–<20 (peer cap under 20). [1][2]
- docs / installation page for v14 (current v14.x docs). [3]
- GitHub issues discussing Angular 19/peerDependency and TypeScript/typedoc conflicts (shows practical incompatibility risks with newer Angular/TS releases). [4][5]
If you must run Angular 20 + TS 5.9, either:
- upgrade to a jest-preset-angular release that explicitly adds Angular 20 support (check for v15 stable/RC notes), or
- use the latest release candidate that advertises support and test thoroughly; otherwise you will hit peer-dependency warnings or runtime/test failures.
Sources:
[1] npmpeer / jest-preset-angular compatibility table.
[2] socket.dev / package diff & v14 changelog.
[3] jest-preset-angular docs (v14 installation).
[4] GitHub issue: Angular 19 support / peerDependencies.
[5] GitHub issue: typescript/typedoc peer-dependency conflicts.
Upgrade jest-preset-angular to one that supports Angular 20 and verify the Jest stack
- Problem: [email protected] is not officially compatible with Angular 20 (peer deps cap <20); package.json shows jest-preset-angular 14.5.3 and typescript 5.9.2, and angular.json uses @angular-builders/jest:run (lines ~112–114).
- Action: upgrade jest-preset-angular to a release that explicitly adds Angular 20 support (v15+ or latest RC), confirm @angular-builders/jest compatibility (upgrade if required), run CI/local tests and fix any test/runtime failures or peer-dependency issues.
🤖 Prompt for AI Agents
In package.json around line 38, update jest-preset-angular (currently 14.5.3) to
a version that explicitly supports Angular 20 (v15+ or the latest stable/RC),
verify and if necessary upgrade @angular-builders/jest to a compatible release,
then run CI/local test suite to catch and fix any test/runtime failures or
peer-dependency issues (adjust typescript or other peer deps as required)
ensuring Jest stack in angular.json (@angular-builders/jest:run) works with the
upgraded presets.
| "iptv-playlist-parser": "github:4gray/iptv-playlist-parser", | ||
| "lodash": "4.17.21", | ||
| "moment": "2.30.1", | ||
| "ngx-indexed-db": "19.0.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Upgrade ngx-indexed-db for Angular 20 peer deps.
v19 targets Angular 19; use the Angular 20-compatible release.
- "ngx-indexed-db": "19.0.0",
+ "ngx-indexed-db": "^20.0.0",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "ngx-indexed-db": "19.0.0", | |
| "ngx-indexed-db": "^20.0.0", |
🤖 Prompt for AI Agents
In package.json line 73, the dependency "ngx-indexed-db": "19.0.0" targets
Angular 19; update it to the Angular 20-compatible release (e.g.,
"ngx-indexed-db": "^20.0.0" or the exact 20.x version required by your app),
then run npm/yarn install to update package-lock.json or yarn.lock and run the
test/build to ensure peer deps resolve cleanly.
| "@angular-eslint/builder": "19.2.0", | ||
| "@angular-eslint/eslint-plugin": "19.2.0", | ||
| "@angular-eslint/eslint-plugin-template": "19.2.0", | ||
| "@angular-eslint/schematics": "19.2.0", | ||
| "@angular-eslint/template-parser": "19.2.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
@angular-eslint v19 + ESLint 9 can conflict.
You’re on ESLint ^9.9.1; upgrade the Angular ESLint suite to v20 to ensure compatibility.
- "@angular-eslint/builder": "19.2.0",
- "@angular-eslint/eslint-plugin": "19.2.0",
- "@angular-eslint/eslint-plugin-template": "19.2.0",
- "@angular-eslint/schematics": "19.2.0",
- "@angular-eslint/template-parser": "19.2.0",
+ "@angular-eslint/builder": "^20.0.0",
+ "@angular-eslint/eslint-plugin": "^20.0.0",
+ "@angular-eslint/eslint-plugin-template": "^20.0.0",
+ "@angular-eslint/schematics": "^20.0.0",
+ "@angular-eslint/template-parser": "^20.0.0",Would you confirm the recommended versions per the latest docs?
🌐 Web query:
What versions of @angular-eslint packages are compatible with ESLint 9 and Angular 20?
💡 Result:
Use @angular-eslint packages with major version 20 (i.e., @angular-eslint/*@^20.0.0). The angular-eslint project aligns its major with Angular, and v18+ added ESLint v9/TypeScript-ESLint v8 flat-config support—so for Angular 20 + ESLint 9 you should use the 20.x @angular-eslint packages. [1][2][3]
Sources:
- npm angular-eslint (notes about v18+ supporting ESLint v9 / typescript-eslint v8). [1]
- angular-eslint GitHub (major version aligns with Angular CLI; supported ESLint versions). [2]
- @angular-eslint/eslint-plugin v20 release (20.x) showing Angular v20 support. [3]
Upgrade @angular-eslint to v20 for ESLint 9 / Angular 20 compatibility
Confirmed: @angular-eslint v20.x is the correct major to use with Angular 20 and ESLint 9 — update package.json dependencies below.
File: package.json
Lines: 86-90
- "@angular-eslint/builder": "19.2.0",
- "@angular-eslint/eslint-plugin": "19.2.0",
- "@angular-eslint/eslint-plugin-template": "19.2.0",
- "@angular-eslint/schematics": "19.2.0",
- "@angular-eslint/template-parser": "19.2.0",
+ "@angular-eslint/builder": "^20.0.0",
+ "@angular-eslint/eslint-plugin": "^20.0.0",
+ "@angular-eslint/eslint-plugin-template": "^20.0.0",
+ "@angular-eslint/schematics": "^20.0.0",
+ "@angular-eslint/template-parser": "^20.0.0",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "@angular-eslint/builder": "19.2.0", | |
| "@angular-eslint/eslint-plugin": "19.2.0", | |
| "@angular-eslint/eslint-plugin-template": "19.2.0", | |
| "@angular-eslint/schematics": "19.2.0", | |
| "@angular-eslint/template-parser": "19.2.0", | |
| "@angular-eslint/builder": "^20.0.0", | |
| "@angular-eslint/eslint-plugin": "^20.0.0", | |
| "@angular-eslint/eslint-plugin-template": "^20.0.0", | |
| "@angular-eslint/schematics": "^20.0.0", | |
| "@angular-eslint/template-parser": "^20.0.0", |
🤖 Prompt for AI Agents
In package.json around lines 86 to 90, the @angular-eslint packages are pinned
to 19.2.0 but need to be upgraded to v20 for Angular 20 and ESLint 9
compatibility; update the versions for "@angular-eslint/builder",
"@angular-eslint/eslint-plugin", "@angular-eslint/eslint-plugin-template",
"@angular-eslint/schematics", and "@angular-eslint/template-parser" to a v20
range (e.g. "^20.0.0" or the specific 20.x release you prefer) and then run your
package manager (npm/yarn/pnpm) to reinstall and verify no peer dependency
conflicts.
| "tslib": "^2.6.2", | ||
| "typescript": "5.9.2", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move tslib to runtime dependencies (Angular apps import helpers at runtime).
tslib should be in "dependencies", not "devDependencies", otherwise builds can reference a package that isn’t installed in production installs.
Apply:
@@ "dependencies": {
"videojs-hls-quality-selector": "1.1.4",
- "zone.js": "~0.15.0"
+ "zone.js": "~0.15.0",
+ "tslib": "^2.6.2"
},
@@ "devDependencies": {
- "tslib": "^2.6.2",
"typescript": "5.9.2",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "tslib": "^2.6.2", | |
| "typescript": "5.9.2", | |
| "dependencies": { | |
| "videojs-hls-quality-selector": "1.1.4", | |
| "zone.js": "~0.15.0", | |
| "tslib": "^2.6.2" | |
| }, | |
| "devDependencies": { | |
| "typescript": "5.9.2", |
🤖 Prompt for AI Agents
In package.json around lines 140-141, tslib is currently listed under
devDependencies but must be moved to dependencies because Angular and other TS
runtime helpers are imported at runtime; update package.json by removing tslib
from devDependencies and adding the same version specifier ("^2.6.2") under the
top-level "dependencies" section, then run npm/yarn install (or update lockfile)
to ensure production installs include tslib.
| if let Some(origin_url) = origin { | ||
| if !origin_url.is_empty() { | ||
| command.arg(format!("--http-origin={}", origin_url)); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
❓ Verification inconclusive
Remove unsupported VLC flag --http-origin; warn and ignore instead.
VLC does not expose an --http-origin CLI option. Known HTTP CLI options include --http-user-agent and --http-referrer; arbitrary headers (like Origin) aren’t supported. Passing an unknown option may be ignored or cause runtime warnings. Replace with a warning and skip applying Origin for VLC.
Apply this diff:
- if let Some(origin_url) = origin {
- if !origin_url.is_empty() {
- command.arg(format!("--http-origin={}", origin_url));
- }
- }
+ if let Some(origin_url) = origin {
+ if !origin_url.is_empty() {
+ log::warn!("VLC does not support setting an HTTP Origin header via CLI; ignoring origin '{}'.", origin_url);
+ }
+ }References: VideoLAN wiki documents http-user-agent and http-referrer but not http-origin. Community threads also note VLC cannot add arbitrary HTTP headers. (wiki.videolan.org)
Remove unsupported VLC flag --http-origin; log warning instead.
VLC’s HTTP input options include --http-user-agent and --http-referrer but not --http-origin (wiki.videolan.org)
File: src-tauri/src/commands/media.rs
@@ -260,7 +260,10 @@
- if let Some(origin_url) = origin {
- if !origin_url.is_empty() {
- command.arg(format!("--http-origin={}", origin_url));
- }
- }
+ if let Some(origin_url) = origin {
+ if !origin_url.is_empty() {
+ log::warn!("VLC does not support setting an HTTP Origin header via CLI; ignoring origin '{}'.", origin_url);
+ }
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if let Some(origin_url) = origin { | |
| if !origin_url.is_empty() { | |
| command.arg(format!("--http-origin={}", origin_url)); | |
| } | |
| } | |
| if let Some(origin_url) = origin { | |
| if !origin_url.is_empty() { | |
| log::warn!("VLC does not support setting an HTTP Origin header via CLI; ignoring origin '{}'.", origin_url); | |
| } | |
| } |
🤖 Prompt for AI Agents
In src-tauri/src/commands/media.rs around lines 260 to 265, remove the
unsupported VLC flag insertion that adds "--http-origin=..."; instead, when an
origin value is present and non-empty, do not append the flag to the command and
emit a warning log indicating the origin option is unsupported by VLC (include
the origin value in the log for debugging). Ensure the change preserves existing
control flow (check for Some(origin) and non-empty) but replaces
command.arg(...) with a call to the project's logging facility (e.g., warn!) to
notify that "--http-origin" is unsupported.
| @if (!isElectron && isDialogVisible$ | async) { | ||
| <ngx-whats-new | ||
| (closeModal)="setDialogVisibility(false)" | ||
| [items]="modals" | ||
| [options]="options" | ||
| /> | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix async pipe precedence to avoid runtime errors
Pipe has the lowest precedence; current expression pipes the entire (!isElectron && isDialogVisible$) into async. Parenthesize the observable.
-@if (!isElectron && isDialogVisible$ | async) {
+@if (!isElectron && (isDialogVisible$ | async)) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @if (!isElectron && isDialogVisible$ | async) { | |
| <ngx-whats-new | |
| (closeModal)="setDialogVisibility(false)" | |
| [items]="modals" | |
| [options]="options" | |
| /> | |
| } | |
| @if (!isElectron && (isDialogVisible$ | async)) { | |
| <ngx-whats-new | |
| (closeModal)="setDialogVisibility(false)" | |
| [items]="modals" | |
| [options]="options" | |
| /> | |
| } |
🤖 Prompt for AI Agents
In src/app/shared/components/header/header.component.html around lines 239 to
245, the current conditional incorrectly applies the async pipe to the whole
expression due to pipe precedence; change the template expression so the async
pipe only applies to the observable by parenthesizing it (e.g. use (!isElectron
&& (isDialogVisible$ | async)) in the *ngIf), ensuring the boolean check
evaluates correctly and preventing runtime errors.
| <button | ||
| mat-icon-button | ||
| (click)="closeStream(process.id)" | ||
| title="Close" | ||
| > | ||
| <mat-icon>close</mat-icon> | ||
| </button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add accessible names to icon-only buttons.
Icon-only “Close” button lacks an explicit accessible name; title is unreliable. Add aria-label and hide the decorative icon from AT.
Apply this diff:
- <button
- mat-icon-button
- (click)="closeStream(process.id)"
- title="Close"
- >
- <mat-icon>close</mat-icon>
- </button>
+ <button
+ mat-icon-button
+ type="button"
+ aria-label="Close stream"
+ (click)="closeStream(process.id)"
+ title="Close"
+ >
+ <mat-icon aria-hidden="true">close</mat-icon>
+ </button>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| mat-icon-button | |
| (click)="closeStream(process.id)" | |
| title="Close" | |
| > | |
| <mat-icon>close</mat-icon> | |
| </button> | |
| <button | |
| mat-icon-button | |
| type="button" | |
| aria-label="Close stream" | |
| (click)="closeStream(process.id)" | |
| title="Close" | |
| > | |
| <mat-icon aria-hidden="true">close</mat-icon> | |
| </button> |
🤖 Prompt for AI Agents
In src/app/shared/components/mpv-player-bar/mpv-player-bar.component.html around
lines 41 to 47, the icon-only Close button lacks an explicit accessible name and
the decorative icon is exposed to assistive tech; add an aria-label (e.g.,
aria-label="Close stream") to the button element and mark the mat-icon
decorative by adding aria-hidden="true" so screen readers read the button label
but ignore the icon; preserve the (click)="closeStream(process.id)" handler and
any title if desired.
| loader: async ({ params }) => { | ||
| if ( | ||
| !request.category || | ||
| request.category === null || | ||
| request.category === '' | ||
| !params.category || | ||
| params.category === null || | ||
| params.category === '' | ||
| ) { | ||
| return Promise.resolve(undefined); | ||
| } | ||
|
|
||
| const currentPlaylist = store.currentPlaylist; | ||
| const params = { | ||
| action: StalkerContentTypes[request.contentType] | ||
| const queryParams = { | ||
| action: StalkerContentTypes[params.contentType] | ||
| .getContentAction, | ||
| type: request.contentType, | ||
| category: request.category ?? '', | ||
| genre: request.category ?? '', | ||
| type: params.contentType, | ||
| category: params.category ?? '', | ||
| genre: params.category ?? '', | ||
| sortby: 'added', | ||
| ...(request.search !== '' | ||
| ? { search: request.search } | ||
| : {}), | ||
| p: request.pageIndex, | ||
| ...(params.search !== '' ? { search: params.search } : {}), | ||
| p: params.pageIndex, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null‑safety and response handling in getContentResource loader.
- Uses response.js before verifying response (Line 186). Critical NPE risk.
- Uses store.currentPlaylist() instead of params.currentPlaylist.
- Minor: simplify empty checks; compute hasMore using known totals without relying on immediate signal update.
Apply this diff:
- loader: async ({ params }) => {
- if (
- !params.category ||
- params.category === null ||
- params.category === ''
- ) {
- return Promise.resolve(undefined);
- }
-
- const currentPlaylist = store.currentPlaylist;
+ loader: async ({ params }) => {
+ if (!params?.category || params.category === '') {
+ return undefined;
+ }
+ if (!params?.currentPlaylist) return undefined;
const queryParams = {
action: StalkerContentTypes[params.contentType]
.getContentAction,
- type: params.contentType,
- category: params.category ?? '',
- genre: params.category ?? '',
+ type: params.contentType,
+ category: params.category ?? '',
+ genre: params.category ?? '',
sortby: 'added',
- ...(params.search !== '' ? { search: params.search } : {}),
+ ...(params.search ? { search: params.search } : {}),
p: params.pageIndex,
};
const response = await dataService.sendIpcEvent(
STALKER_REQUEST,
{
- url: currentPlaylist().portalUrl,
- macAddress: currentPlaylist().macAddress,
+ url: params.currentPlaylist.portalUrl,
+ macAddress: params.currentPlaylist.macAddress,
params: queryParams,
}
);
- patchState(store, { totalCount: response.js.total_items });
+ if (!response) {
+ throw new Error('Error: no response from STALKER_REQUEST');
+ }
+ const totalItems = Number(response.js?.total_items ?? 0);
+ patchState(store, { totalCount: totalItems });
- if (response) {
- const newItems = response.js.data.map((item) => ({
+ if (response) {
+ const data = Array.isArray(response.js?.data) ? response.js.data : [];
+ const newItems = data.map((item) => ({
...item,
cover: item.screenshot_uri,
}));
if (store.selectedContentType() === 'itv') {
// Check if we're loading the first page or loading more
- if (params.pageIndex === 1) {
+ if (params.pageIndex === 1) {
patchState(store, { itvChannels: newItems });
} else {
patchState(store, {
itvChannels: [
...store.itvChannels(),
...newItems,
],
});
}
// Update hasMoreItems based on total count and current items
- const totalLoaded = store.itvChannels().length;
+ const totalLoaded =
+ params.pageIndex === 1
+ ? newItems.length
+ : store.itvChannels().length + newItems.length;
patchState(store, {
- hasMoreChannels:
- totalLoaded < response.js.total_items,
+ hasMoreChannels: totalLoaded < totalItems,
});
}
return newItems;
- } else {
- throw new Error(
- `Error: ${response.message} (Status: ${response.status})`
- );
- }
+ }Also applies to: 180-186, 188-213
🤖 Prompt for AI Agents
In src/app/stalker/stalker.store.ts around lines 156-175 (also applies to
180-186 and 188-213), the loader needs null-safety and correct playlist usage:
replace the verbose category emptiness checks with a simple falsy check (e.g. if
(!params.category) return undefined), use params.currentPlaylist instead of
store.currentPlaylist, guard usage of response.js/response.json by first
checking response.ok and that response is non-null before parsing, and compute
hasMore from returned totals (e.g. compare total items to pageIndex*pageSize)
rather than relying on immediate signal updates; ensure error/empty responses
return a safe undefined or empty result and log or throw a descriptive error
where appropriate.
| (error)=" | ||
| $event.target.src = './assets/images/default-poster.png' | ||
| " | ||
| /> | ||
| } @else { | ||
| <div class="placeholder-cover"></div> | ||
| } | ||
| </div> | ||
| <div class="details"> | ||
| <h2>{{ item.info.name }}</h2> | ||
| <label *ngIf="item.info.description"> | ||
| {{ item.info.description }} | ||
| </label> | ||
| <label *ngIf="item.info.releasedate"> | ||
| <div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div> | ||
| {{ item.info.releasedate }} | ||
| </label> | ||
| <label *ngIf="item.info.genre"> | ||
| <div class="label">{{ 'XTREAM.GENRE' | translate }}:</div> | ||
| {{ item.info.genre }} | ||
| </label> | ||
| <label *ngIf="item.info.country"> | ||
| <div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div> | ||
| {{ item.info.country }} | ||
| </label> | ||
| <div *ngIf="item.info.actors"> | ||
| <div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div> | ||
| {{ item.info.actors }} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix template type error in (error) handler.
$event.target is EventTarget and has no src in strict template checking. Cast or delegate to a method.
- (error)="
- $event.target.src = './assets/images/default-poster.png'
- "
+ (error)="($event.target as HTMLImageElement).src = './assets/images/default-poster.png'"Or: (error)="onImgError($event)" and set (event.target as HTMLImageElement).src in TS.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/app/xtream/vod-details/vod-details.component.html around lines 15-18 the
(error) handler tries to set $event.target.src but template type checking sees
$event.target as EventTarget without a src property; change the template to call
a component method (e.g. (error)="onImgError($event)") or cast the target
in-line, and implement the onImgError handler in the component TS to set
(event.target as HTMLImageElement).src = 'assets/images/default-poster.png' (or
otherwise guard/cast before assigning).
| } | ||
| </div> | ||
| <div class="details"> | ||
| <h2>{{ item.info.name }}</h2> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against undefined item.info throughout (@if conditions and interpolations).
Several blocks access item.info.* without safe navigation, which will throw if info is undefined during initial render. Use ?. in conditions and interpolations or wrap the section with a single guard (@if (item?.info as info) { ... }).
Apply this minimal fix:
- <h2>{{ item.info.name }}</h2>
+ <h2>{{ item.info?.name }}</h2>
- @if (item.info.description) {
+ @if (item.info?.description) {
- {{ item.info.description }}
+ {{ item.info?.description }}
}
- @if (item.info.releasedate) {
+ @if (item.info?.releasedate) {
- {{ item.info.releasedate }}
+ {{ item.info?.releasedate }}
}
- @if (item.info.genre) {
+ @if (item.info?.genre) {
- {{ item.info.genre }}
+ {{ item.info?.genre }}
}
- @if (item.info.country) {
+ @if (item.info?.country) {
- {{ item.info.country }}
+ {{ item.info?.country }}
}
- @if (item.info.actors) {
+ @if (item.info?.actors) {
- {{ item.info.actors }}
+ {{ item.info?.actors }}
}
- @if (item.info.director) {
+ @if (item.info?.director) {
- {{ item.info.director }}
+ {{ item.info?.director }}
}
- @if (item.info.duration) {
+ @if (item.info?.duration) {
- {{ item.info.duration }}
+ {{ item.info?.duration }}
}
- @if (item.info.rating_imdb) {
+ @if (item.info?.rating_imdb) {
- {{ item.info.rating_imdb }}
+ {{ item.info?.rating_imdb }}
}
- @if (item.info.rating_kinopoisk) {
+ @if (item.info?.rating_kinopoisk) {
- {{ item.info.rating_kinopoisk }}
+ {{ item.info?.rating_kinopoisk }}
}
- @if (item.info.youtube_trailer) {
+ @if (item.info?.youtube_trailer) {
- 'https://www.youtube.com/embed/' + item.info?.youtube_trailer | safe
+ 'https://www.youtube.com/embed/' + item.info?.youtube_trailer | safe
}Alternatively, wrap once and alias:
@if (item?.info as info) {
<h2>{{ info.name }}</h2>
...
[src]="'https://www.youtube.com/embed/' + info.youtube_trailer | safe"
}Also applies to: 25-29, 30-35, 36-41, 42-47, 48-53, 54-59, 60-65, 66-71, 72-79, 112-128
🤖 Prompt for AI Agents
In src/app/xtream/vod-details/vod-details.component.html around line 24 (and
also covering lines
25-29,30-35,36-41,42-47,48-53,54-59,60-65,66-71,72-79,112-128), multiple
bindings access item.info.* directly which will throw if item.info is undefined;
update the template to guard against undefined by either adding safe-navigation
operators to each access (e.g., item?.info?.prop) or, preferably, wrap the whole
section in a single guard that aliases info (e.g., @if (item?.info as info) {
... } ) and then use info.prop throughout, and ensure any attribute bindings
(like [src]) use the aliased info and existing pipes safely.
loading-overlay, navigation-bar, season-container, serial-details, and
vod-details components.
Summary by CodeRabbit
New Features
UI Changes
Breaking Changes
Style
Chores