fix(xtream): enhance error handling in content hero and serial details components

Entire-Checkpoint: 3ba3a7503b59
This commit is contained in:
4gray committed 2026-02-11 22:50:33 +01:00
1 parent c06feb1ff6
commit 166e81dae0
10 files changed
+88 -19

No files matched your search

@@ -74,6 +74,11 @@
(episodeClicked)="playEpisode($event)"
/>
</app-content-hero>
} @else if (detailsError()) {
<app-content-hero
[errorMessage]="detailsError()"
(backClicked)="goBack()"
/>
} @else {
<app-content-hero [isLoading]="true" (backClicked)="goBack()" />
}
@@ -28,6 +28,8 @@ export class SerialDetailsComponent implements OnInit, OnDestroy {
readonly selectedItem = this.xtreamStore.selectedItem;
readonly selectedContentType = this.xtreamStore.selectedContentType;
readonly isFavorite = this.xtreamStore.isFavorite;
readonly isLoadingDetails = this.xtreamStore.isLoadingDetails;
readonly detailsError = this.xtreamStore.detailsError;
ngOnInit(): void {
const { categoryId, serialId } = this.route.snapshot.params;
@@ -271,6 +271,14 @@ export class XtreamApiService {
url,
params: serializedParams,
});
// The IPC layer catches errors and returns { type: 'ERROR', message, status }
// instead of rejecting. Convert that back into a thrown error so callers
// can handle it with .catch() / try-catch.
if (response?.type === 'ERROR' || (!response?.payload && response?.message)) {
throw new Error(response?.message ?? 'Request failed');
}
return response?.payload;
}
}
@@ -18,6 +18,7 @@ export interface SelectionState {
page: number;
limit: number;
isLoadingDetails: boolean;
detailsError: string | null;
}
/**
@@ -30,6 +31,7 @@ const initialSelectionState: SelectionState = {
page: 0,
limit: Number(localStorage.getItem('xtream-page-size') ?? 25),
isLoadingDetails: false,
detailsError: null,
};
/**
@@ -308,6 +310,13 @@ export function withSelection() {
patchState(store, { isLoadingDetails: isLoading });
},
/**
* Set the details error state
*/
setDetailsError(error: string | null): void {
patchState(store, { detailsError: error });
},
/**
* Set the current page
*/
@@ -131,6 +131,7 @@ export const XtreamStore = signalStore(
if (!playlist) return;
store.setIsLoadingDetails(true);
store.setDetailsError(null);
xtreamApiService.getVodInfo({
serverUrl: playlist.serverUrl,
username: playlist.username,
@@ -143,6 +144,9 @@ export const XtreamStore = signalStore(
});
}).catch((error: unknown) => {
console.error('Error fetching VOD details:', error);
store.setDetailsError(
error instanceof Error ? error.message : 'Unknown error'
);
}).finally(() => {
store.setIsLoadingDetails(false);
});
@@ -159,6 +163,7 @@ export const XtreamStore = signalStore(
if (!playlist) return;
store.setIsLoadingDetails(true);
store.setDetailsError(null);
xtreamApiService.getSeriesInfo({
serverUrl: playlist.serverUrl,
username: playlist.username,
@@ -171,6 +176,9 @@ export const XtreamStore = signalStore(
});
}).catch((error: unknown) => {
console.error('Error fetching series details:', error);
store.setDetailsError(
error instanceof Error ? error.message : 'Unknown error'
);
}).finally(() => {
store.setIsLoadingDetails(false);
});
@@ -135,6 +135,11 @@
</div>
}
</app-content-hero>
} @else if (detailsError()) {
<app-content-hero
[errorMessage]="detailsError()"
(backClicked)="goBack()"
/>
} @else {
<app-content-hero [isLoading]="true" (backClicked)="goBack()" />
}
@@ -45,6 +45,8 @@ export class VodDetailsRouteComponent implements OnInit, OnDestroy {
readonly isFavorite = this.xtreamStore.isFavorite;
readonly selectedItem = this.xtreamStore.selectedItem;
readonly isLoadingDetails = this.xtreamStore.isLoadingDetails;
readonly detailsError = this.xtreamStore.detailsError;
readonly hasPlaybackPosition = computed(() => {
const vodId = this.route.snapshot.params.vodId;
@@ -4,7 +4,13 @@
<button class="hero__back-button" (click)="onBack()">
<mat-icon>arrow_back</mat-icon>
</button>
@if (isLoading) {
@if (errorMessage()) {
<div class="hero__content hero__error">
<mat-icon class="error-icon">cloud_off</mat-icon>
<h2 class="error-title">{{ errorMessage() }}</h2>
<ng-content select="[hero-error-actions]"></ng-content>
</div>
} @else if (isLoading()) {
<div class="hero__content">
<!-- Skeleton Poster -->
<div class="poster">
@@ -85,12 +91,12 @@
<!-- Backdrop Image -->
<div
class="hero__backdrop"
[class.hero__backdrop--blurred]="!backdropUrl"
[class.hero__backdrop--blurred]="!backdropUrl()"
[style.backgroundImage]="
backdropUrl
? 'url(' + backdropUrl + ')'
: posterUrl
? 'url(' + posterUrl + ')'
backdropUrl()
? 'url(' + backdropUrl() + ')'
: posterUrl()
? 'url(' + posterUrl() + ')'
: 'none'
"
></div>
@@ -102,10 +108,10 @@
<div class="hero__content">
<!-- Poster Card -->
<div class="poster">
@if (posterUrl) {
@if (posterUrl()) {
<img
class="poster__image"
[src]="posterUrl"
[src]="posterUrl()"
(error)="
$event.target.src =
'./assets/images/default-poster.png'
@@ -119,7 +125,7 @@
<!-- Details -->
<div class="details">
<h1 class="details__title">{{ title }}</h1>
<h1 class="details__title">{{ title() }}</h1>
<!-- Tags Slot -->
<div class="details__tags">
@@ -127,9 +133,9 @@
</div>
<!-- Description -->
@if (description) {
@if (description()) {
<p class="details__description">
{{ description }}
{{ description() }}
</p>
}
@@ -148,4 +154,4 @@
</div>
</div>
}
</section>
</section>
@@ -179,6 +179,29 @@
display: none;
}
}
// Error state
&__error {
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 16px;
.error-icon {
font-size: 64px;
width: 64px;
height: 64px;
color: var(--text-muted);
}
.error-title {
font-size: 1.25rem;
font-weight: 500;
color: var(--text-secondary);
margin: 0;
}
}
}
// ============================================================================
@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, input, output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
@@ -17,13 +17,14 @@ import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader';
styleUrls: ['./content-hero.component.scss'],
})
export class ContentHeroComponent {
@Input() title: string | undefined;
@Input() description: string | undefined;
@Input() posterUrl: string | undefined;
@Input() backdropUrl: string | undefined;
@Input() isLoading = false;
readonly title = input<string>();
readonly description = input<string>();
readonly posterUrl = input<string>();
readonly backdropUrl = input<string>();
readonly isLoading = input(false);
readonly errorMessage = input<string>();
@Output() backClicked = new EventEmitter<void>();
readonly backClicked = output<void>();
onBack(): void {
this.backClicked.emit();