mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
fix(xtream): enhance error handling in content hero and serial details components
Entire-Checkpoint: 3ba3a7503b59
This commit is contained in:
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();
|
||||
|
||||
Reference in new issue
Block a user