diff --git a/apps/web/src/app/stalker/stalker-main-container.component.html b/apps/web/src/app/stalker/stalker-main-container.component.html
index 865406fd8..c1309723a 100644
--- a/apps/web/src/app/stalker/stalker-main-container.component.html
+++ b/apps/web/src/app/stalker/stalker-main-container.component.html
@@ -17,50 +17,41 @@
}
@if (isItvCategorySelected) {
-
- @for (item of itvChannels(); track $index) {
-
-
- {{ item.name }}
-
-
- }
-
+
+
+ @for (item of itvChannels(); track $index) {
+
+
+ {{ item.name }}
+
+
+ }
+
- @if (hasMoreItems() && !isContentLoading()) {
-
-
-
-
- }
+
+ @if (isLoadingMore()) {
+
+
+
+ }
+
} @else {
@if (isCategoryLoading()) {
('itvScrollContainer');
+ private scrollListener: (() => void) | null = null;
+
readonly selectedCategoryTitle = this.stalkerStore.getSelectedCategoryName;
readonly currentPlaylist = this.stalkerStore.currentPlaylist;
@@ -158,10 +173,22 @@ export class StalkerMainContainerComponent {
//this.hasMoreItems.set(false);
});
- // reset loading state when content resource changes
+ // reset loading state when content resource changes and check if we need more
effect(() => {
if (this.contentItems() !== undefined) {
this.isLoadingMore.set(false);
+ // Check if we need to load more to fill the viewport
+ this.checkIfNeedsMoreContent();
+ }
+ });
+
+ // Also check when ITV channels are loaded
+ effect(() => {
+ const channels = this.itvChannels();
+ if (channels.length > 0) {
+ this.isLoadingMore.set(false);
+ // Small delay to allow DOM to update before checking
+ setTimeout(() => this.checkIfNeedsMoreContent(), 100);
}
});
@@ -172,6 +199,66 @@ export class StalkerMainContainerComponent {
this.favorites.set(fav.id, true);
});
});
+
+ // Setup scroll listener when container becomes available
+ effect(() => {
+ const scrollContainer = this.itvScrollContainer();
+ if (scrollContainer) {
+ this.setupScrollListener();
+ }
+ });
+ }
+
+ ngAfterViewInit() {
+ this.setupScrollListener();
+ }
+
+ ngOnDestroy() {
+ this.removeScrollListener();
+ }
+
+ private setupScrollListener() {
+ this.removeScrollListener();
+
+ const container = this.itvScrollContainer()?.nativeElement;
+ if (!container) return;
+
+ const onScroll = () => {
+ if (this.isLoadingMore() || !this.hasMoreItems()) return;
+
+ const { scrollTop, scrollHeight, clientHeight } = container;
+ const scrollThreshold = 150; // pixels from bottom to trigger load
+
+ if (scrollHeight - scrollTop - clientHeight <= scrollThreshold) {
+ this.loadMore();
+ }
+ };
+
+ container.addEventListener('scroll', onScroll, { passive: true });
+ this.scrollListener = () => container.removeEventListener('scroll', onScroll);
+ }
+
+ /**
+ * Check if the container needs more content to become scrollable.
+ * If content doesn't fill the viewport and there are more items, load more.
+ */
+ private checkIfNeedsMoreContent() {
+ const container = this.itvScrollContainer()?.nativeElement;
+ if (!container) return;
+ if (this.isLoadingMore() || !this.hasMoreItems()) return;
+
+ const { scrollHeight, clientHeight } = container;
+ // If content doesn't fill the container (no scrollbar), load more
+ if (scrollHeight <= clientHeight) {
+ this.loadMore();
+ }
+ }
+
+ private removeScrollListener() {
+ if (this.scrollListener) {
+ this.scrollListener();
+ this.scrollListener = null;
+ }
}
handshake() {
diff --git a/apps/web/src/app/xtream-tauri/season-container/season-container.component.ts b/apps/web/src/app/xtream-tauri/season-container/season-container.component.ts
index 2e4ba47f4..43c2257f5 100644
--- a/apps/web/src/app/xtream-tauri/season-container/season-container.component.ts
+++ b/apps/web/src/app/xtream-tauri/season-container/season-container.component.ts
@@ -60,6 +60,44 @@ export class SeasonContainerComponent {
return this.xtreamStore.currentPlaylist()?.id;
}
+ /**
+ * Get a stable numeric ID for an episode (used for downloads tracking)
+ * For regular-series, episode.id contains cmd string, so we extract ID from it
+ * For vod-series and xtream, episode.id is already numeric
+ */
+ private getEpisodeDownloadId(episode: XtreamSerieEpisode): number {
+ const customSid = (episode as any).custom_sid;
+
+ if (customSid === 'regular-series') {
+ // For regular-series, episode.id is cmd like "/media/file_123.mpg"
+ // Extract the numeric part if possible
+ const cmd = episode.id as unknown as string;
+ const match = cmd?.match(/file_(\d+)/);
+ if (match) {
+ return Number(match[1]);
+ }
+ // Fallback: hash the cmd string to get a consistent number
+ return this.hashString(cmd || '');
+ }
+
+ // For vod-series and xtream, use the numeric id directly
+ const numId = Number(episode.id);
+ return isNaN(numId) ? this.hashString(String(episode.id)) : numId;
+ }
+
+ /**
+ * Simple string hash function to generate consistent numeric IDs
+ */
+ private hashString(str: string): number {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ const char = str.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash = hash & hash; // Convert to 32-bit integer
+ }
+ return Math.abs(hash);
+ }
+
@Output() episodeClicked = new EventEmitter();
@Output() seasonSelected = new EventEmitter();
@@ -259,7 +297,7 @@ export class SeasonContainerComponent {
await this.downloadsService.startDownload({
playlistId: playlist._id,
- xtreamId: Number(episode.id) || Date.now(), // Use timestamp if id is not numeric
+ xtreamId: this.getEpisodeDownloadId(episode),
contentType: 'episode',
title: episodeTitle,
url,
@@ -287,7 +325,7 @@ export class SeasonContainerComponent {
// Access downloads signal to make this reactive
this.downloadsService.downloads();
return this.downloadsService.isDownloaded(
- Number(episode.id),
+ this.getEpisodeDownloadId(episode),
playlistId,
'episode'
);
@@ -300,7 +338,7 @@ export class SeasonContainerComponent {
// Access downloads signal to make this reactive
this.downloadsService.downloads();
return this.downloadsService.isDownloading(
- Number(episode.id),
+ this.getEpisodeDownloadId(episode),
playlistId,
'episode'
);
@@ -314,7 +352,7 @@ export class SeasonContainerComponent {
if (!playlistId) return;
const filePath = this.downloadsService.getDownloadedFilePath(
- Number(episode.id),
+ this.getEpisodeDownloadId(episode),
playlistId,
'episode'
);