diff --git a/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.html b/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.html
index ffc0151e1..3ab6e4de6 100644
--- a/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.html
+++ b/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.html
@@ -74,6 +74,11 @@
(episodeClicked)="playEpisode($event)"
/>
+} @else if (detailsError()) {
+
} @else {
}
diff --git a/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.ts b/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.ts
index 07a6a41b1..c38ca3ef9 100644
--- a/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.ts
+++ b/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.ts
@@ -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;
diff --git a/apps/web/src/app/xtream-tauri/services/xtream-api.service.ts b/apps/web/src/app/xtream-tauri/services/xtream-api.service.ts
index b2f5274c9..d50246601 100644
--- a/apps/web/src/app/xtream-tauri/services/xtream-api.service.ts
+++ b/apps/web/src/app/xtream-tauri/services/xtream-api.service.ts
@@ -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;
}
}
diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts
index 0a6f509ac..55c22cfb4 100644
--- a/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts
+++ b/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts
@@ -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
*/
diff --git a/apps/web/src/app/xtream-tauri/stores/xtream.store.ts b/apps/web/src/app/xtream-tauri/stores/xtream.store.ts
index a7ad98b40..5516500e5 100644
--- a/apps/web/src/app/xtream-tauri/stores/xtream.store.ts
+++ b/apps/web/src/app/xtream-tauri/stores/xtream.store.ts
@@ -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);
});
diff --git a/apps/web/src/app/xtream-tauri/vod-details/vod-details-route.component.html b/apps/web/src/app/xtream-tauri/vod-details/vod-details-route.component.html
index 2efde06a6..385a3e29a 100644
--- a/apps/web/src/app/xtream-tauri/vod-details/vod-details-route.component.html
+++ b/apps/web/src/app/xtream-tauri/vod-details/vod-details-route.component.html
@@ -135,6 +135,11 @@
}
+} @else if (detailsError()) {
+
} @else {
}
diff --git a/apps/web/src/app/xtream-tauri/vod-details/vod-details-route.component.ts b/apps/web/src/app/xtream-tauri/vod-details/vod-details-route.component.ts
index b31b0ef4c..dedcd17f4 100644
--- a/apps/web/src/app/xtream-tauri/vod-details/vod-details-route.component.ts
+++ b/apps/web/src/app/xtream-tauri/vod-details/vod-details-route.component.ts
@@ -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;
diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.html b/libs/ui/components/src/lib/content-hero/content-hero.component.html
index 872e143ed..9c3bb8e1e 100644
--- a/libs/ui/components/src/lib/content-hero/content-hero.component.html
+++ b/libs/ui/components/src/lib/content-hero/content-hero.component.html
@@ -4,7 +4,13 @@
- @if (isLoading) {
+ @if (errorMessage()) {
+
+ cloud_off
+
{{ errorMessage() }}
+
+
+ } @else if (isLoading()) {
@@ -85,12 +91,12 @@
@@ -102,10 +108,10 @@
- @if (posterUrl) {
+ @if (posterUrl()) {
-
{{ title }}
+
{{ title() }}
@@ -127,9 +133,9 @@
- @if (description) {
+ @if (description()) {
- {{ description }}
+ {{ description() }}
}
@@ -148,4 +154,4 @@
}
-
\ No newline at end of file
+
diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.scss b/libs/ui/components/src/lib/content-hero/content-hero.component.scss
index d1fe1cb28..93d725ce6 100644
--- a/libs/ui/components/src/lib/content-hero/content-hero.component.scss
+++ b/libs/ui/components/src/lib/content-hero/content-hero.component.scss
@@ -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;
+ }
+ }
}
// ============================================================================
diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.ts b/libs/ui/components/src/lib/content-hero/content-hero.component.ts
index 2dcfff209..75fb970c2 100644
--- a/libs/ui/components/src/lib/content-hero/content-hero.component.ts
+++ b/libs/ui/components/src/lib/content-hero/content-hero.component.ts
@@ -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
();
+ readonly description = input();
+ readonly posterUrl = input();
+ readonly backdropUrl = input();
+ readonly isLoading = input(false);
+ readonly errorMessage = input();
- @Output() backClicked = new EventEmitter();
+ readonly backClicked = output();
onBack(): void {
this.backClicked.emit();