- @if (currentLayout === 'category_content') {
-
- @for (a of [].constructor(14); track $index) {
-
-
-
-
- }
-
- } @else if (currentLayout === 'category') {
-
- @for (a of [].constructor(14); track $index) {
-
- }
-
+
+
+
diff --git a/src/app/stalker/stalker-main-container.component.scss b/src/app/stalker/stalker-main-container.component.scss
index 18123a03f..04e7f9e80 100644
--- a/src/app/stalker/stalker-main-container.component.scss
+++ b/src/app/stalker/stalker-main-container.component.scss
@@ -1,3 +1,5 @@
+@use '@angular/material' as mat;
+
:host {
display: flex;
flex-direction: column;
@@ -24,3 +26,20 @@
.category-view {
gap: 10px;
}
+
+.sidebar-header {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ button {
+ @include mat.form-field-density(-5);
+ }
+}
+
+.content-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
diff --git a/src/app/stalker/stalker-main-container.component.ts b/src/app/stalker/stalker-main-container.component.ts
index 97c07909e..d6c229702 100644
--- a/src/app/stalker/stalker-main-container.component.ts
+++ b/src/app/stalker/stalker-main-container.component.ts
@@ -1,642 +1,238 @@
-import { AsyncPipe } from '@angular/common';
-import { Component, NgZone, OnInit, Signal, effect } from '@angular/core';
-import { toSignal } from '@angular/core/rxjs-interop';
-import { MatDialog } from '@angular/material/dialog';
-import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
+import { Component, effect, inject, signal } from '@angular/core';
+import { MatButton, MatIconButton } from '@angular/material/button';
+import { MatIcon } from '@angular/material/icon';
+import { MatListModule } from '@angular/material/list';
+import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSnackBar } from '@angular/material/snack-bar';
-import { ActivatedRoute, Params, Router } from '@angular/router';
-import { Store } from '@ngrx/store';
-import { StorageMap } from '@ngx-pwa/local-storage';
-import { TranslateModule, TranslateService } from '@ngx-translate/core';
+import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
+import { TranslatePipe } from '@ngx-translate/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
-import { Observable } from 'rxjs';
-import { IpcCommand } from '../../../shared/ipc-command.class';
-import {
- ERROR,
- OPEN_MPV_PLAYER,
- OPEN_VLC_PLAYER,
- STALKER_REQUEST,
- STALKER_RESPONSE,
-} from '../../../shared/ipc-commands';
-import { StalkerPortalActions } from '../../../shared/stalker-portal-actions.enum';
-import { DataService } from '../services/data.service';
import { PlaylistsService } from '../services/playlists.service';
-import { Settings, VideoPlayer } from '../settings/settings.interface';
-import { ExternalPlayerInfoDialogComponent } from '../shared/components/external-player-info-dialog/external-player-info-dialog.component';
-import { STORE_KEY } from '../shared/enums/store-keys.enum';
-import { selectCurrentPlaylist } from '../state/selectors';
-import { Breadcrumb } from '../xtream/breadcrumb.interface';
-import { CategoryContentViewComponent } from '../xtream/category-content-view/category-content-view.component';
-import { CategoryViewComponent } from '../xtream/category-view/category-view.component';
-import { ContentTypeNavigationItem } from '../xtream/content-type-navigation-item.interface';
-import { ContentType } from '../xtream/content-type.enum';
-import { NavigationBarComponent } from '../xtream/navigation-bar/navigation-bar.component';
-import {
- PlayerDialogComponent,
- PlayerDialogData,
-} from '../xtream/player-dialog/player-dialog.component';
+import { MpvPlayerBarComponent } from '../shared/components/mpv-player-bar/mpv-player-bar.component';
+import { CategoryViewComponent } from '../xtream-tauri/category-view/category-view.component';
import { PlaylistErrorViewComponent } from '../xtream/playlist-error-view/playlist-error-view.component';
-import { PortalStore } from '../xtream/portal.store';
-import { VodDetailsComponent } from '../xtream/vod-details/vod-details.component';
-import {
- StalkerFavoriteItem,
- StalkerSeason,
- StalkerVodDetails,
-} from './models';
-import { StalkerContentTypes } from './stalker-content-types';
-import { StalkerSeriesViewComponent } from './stalker-series-view/stalker-series-view.component';
+import { StalkerStore } from './stalker.store';
@Component({
selector: 'app-stalker-main-container',
templateUrl: './stalker-main-container.component.html',
- styleUrl: './stalker-main-container.component.scss',
+ styleUrls: [
+ './stalker-main-container.component.scss',
+ '../xtream-tauri/xtream-main-container.component.scss',
+ '../xtream-tauri/sidebar.scss',
+ ],
imports: [
- AsyncPipe,
- CategoryContentViewComponent,
CategoryViewComponent,
+ MatButton,
+ MatIcon,
+ MatIconButton,
+ MatListModule,
MatPaginatorModule,
- NavigationBarComponent,
+ MpvPlayerBarComponent,
NgxSkeletonLoaderModule,
PlaylistErrorViewComponent,
- StalkerSeriesViewComponent,
- TranslateModule,
- VodDetailsComponent,
- ]
+ TranslatePipe,
+ RouterOutlet,
+ ],
})
-export class StalkerMainContainerComponent implements OnInit {
- breadcrumbs: Breadcrumb[] = [];
- currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
- listeners = [];
- isLoading = true;
- selectedContentType = ContentType.VODS;
+export class StalkerMainContainerComponent {
+ readonly stalkerStore = inject(StalkerStore);
+
currentLayout:
- | 'category'
| 'category_content'
- | 'favorites'
| 'serial-details'
| 'vod-details'
- | 'not-available' = 'category';
- searchPhrase = this.portalStore.searchPhrase();
- settings = toSignal(
- this.storage.get(STORE_KEY.Settings)
- ) as Signal
;
- favorites$: Observable;
+ | 'not-available' = 'category_content';
- items: any[] = [];
- itemDetails!: StalkerVodDetails;
+ /* searchPhrase = this.portalStore.searchPhrase(); */
- navigationContentTypes: ContentTypeNavigationItem[] = [
- {
- contentType: ContentType.ITV,
- label: 'Live Streams',
- icon: 'live_tv',
- },
- {
- contentType: ContentType.VODS,
- label: 'VOD Streams',
- icon: 'movie',
- },
- {
- contentType: ContentType.SERIES,
- label: 'Series',
- icon: 'video_library',
- },
- /* {
- contentType: ContentType.RADIO,
- label: 'Radio',
- }, */
- ];
+ readonly pageIndex = this.stalkerStore.page;
+ readonly isLoadingMore = signal(false);
+ readonly favorites = new Map();
- hideExternalInfoDialog = this.portalStore.hideExternalInfoDialog;
+ /* load more functionality */
+ readonly hasMoreItems = this.stalkerStore.hasMoreChannels;
+ readonly itvChannels = this.stalkerStore.itvChannels;
- currentCategoryId;
+ readonly selectedCategoryTitle = this.stalkerStore.getSelectedCategoryName;
- //pagination
- length = 0;
- pageSize = 14;
- pageIndex = 0;
+ /** categories */
+ readonly categories = this.stalkerStore.getCategoryResource;
+ readonly isCategoryLoading = this.stalkerStore.isCategoryResourceLoading;
+ readonly isCategoryFailed = this.stalkerStore.isCategoryResourceFailed;
- itvTitle: string;
+ /** content items */
+ readonly contentItems = this.stalkerStore.getPaginatedContent;
+ readonly isContentLoading = this.stalkerStore.isPaginatedContentLoading;
- seasons: StalkerSeason[] = [];
-
- commandsList = [
- new IpcCommand(STALKER_RESPONSE, (response: any) =>
- this.handleResponse(response)
- ),
- new IpcCommand(
- ERROR,
- (response: { message: string; status: number }) => {
- this.currentLayout = 'not-available';
- this.showErrorAsNotification(response);
- this.isLoading = false;
+ /* readonly contentResource = resource({
+ request: () => ({
+ contentType: this.stalkerStore.selectedContentType(),
+ category: this.stalkerStore.selectedCategoryId(),
+ action: StalkerPortalActions.GetOrderedList,
+ search: this.searchPhrase,
+ pageIndex: this.pageIndex(),
+ }),
+ loader: async ({ request }) => {
+ if (
+ !request.category ||
+ request.category === null ||
+ request.category === ''
+ ) {
+ return Promise.resolve(undefined);
}
- ),
- ];
+
+ const { portalUrl, macAddress } = this.currentPlaylist();
+ const params = {
+ action: StalkerContentTypes[request.contentType]
+ .getContentAction,
+ type: request.contentType,
+ category: request.category ?? '',
+ genre: request.category ?? '',
+ //sortby: 'added',
+ ...(request.search !== '' ? { search: request.search } : {}),
+ p: request.pageIndex,
+ };
+
+ const response = await this.dataService.sendIpcEvent(
+ STALKER_REQUEST,
+ {
+ url: portalUrl,
+ macAddress,
+ params,
+ }
+ );
+
+ if (response) {
+ const newItems = response.js.data.map((item) => ({
+ ...item,
+ cover: item.screenshot_uri,
+ }));
+
+ // Check if we're loading the first page or loading more
+ if (request.pageIndex === 0) {
+ this.itvChannels.set(newItems);
+ } else {
+ // Append new items to existing ones
+ this.itvChannels.update((items) => [
+ ...items,
+ ...newItems,
+ ]);
+ }
+
+ // Update hasMoreItems based on total count and current items
+ const totalLoaded = this.itvChannels().length;
+ this.hasMoreItems.set(totalLoaded < response.js.total_items);
+
+ return newItems;
+ } else {
+ this.currentLayout = 'not-available';
+ this.showErrorAsNotification({
+ message: 'Error',
+ status: 500,
+ });
+ throw new Error(
+ `Error: ${response.message} (Status: ${response.status})`
+ );
+ }
+ },
+ }); */
constructor(
- private activatedRoute: ActivatedRoute,
- private dataService: DataService,
- private dialog: MatDialog,
- private ngZone: NgZone,
- private playlistService: PlaylistsService,
- private portalStore: PortalStore,
- private router: Router,
- private snackBar: MatSnackBar,
- private storage: StorageMap,
- private store: Store,
- private translate: TranslateService
+ private readonly activatedRoute: ActivatedRoute,
+ private readonly playlistService: PlaylistsService,
+ private readonly router: Router,
+ private readonly snackBar: MatSnackBar
) {
- effect(
- () => {
- if (this.currentPlaylist()) {
- if (
- this.currentPlaylist().password &&
- this.currentPlaylist().username
- ) {
- this.handshake();
- } else {
- this.selectedContentType =
- this.activatedRoute.snapshot.queryParams.type ??
- ContentType.VODS;
+ // reset category title after changing content type
+ effect(() => {
+ this.stalkerStore.selectedContentType();
+ });
- const action =
- this.activatedRoute.snapshot.queryParams.action ??
- StalkerPortalActions.GetCategories;
- const { category, movie_id } =
- this.activatedRoute.snapshot.queryParams;
+ // reset items when category changes
+ effect(() => {
+ this.stalkerStore.selectedCategoryId();
+ this.stalkerStore.setItvChannels([]);
+ this.stalkerStore.setPage(0);
+ //this.hasMoreItems.set(false);
+ });
- if (action === StalkerPortalActions.GetCategories) {
- this.getCategories(this.selectedContentType);
- } else if (
- action === StalkerPortalActions.GetOrderedList &&
- (category || movie_id)
- ) {
- this.getOrderedList(
- this.selectedContentType,
- category,
- movie_id
- );
- }
- }
-
- this.favorites$ = this.playlistService.getPortalFavorites(
- this.currentPlaylist()._id
- );
- }
- },
- { allowSignalWrites: true }
- );
-
- this.portalStore.setSearchPhrase('');
- }
-
- ngOnInit() {
- this.commandsList.forEach((command) => {
- if (this.dataService.isElectron) {
- this.dataService.listenOn(command.id, (_event, response) =>
- this.ngZone.run(() => command.callback(response))
- );
- } else {
- const cb = (response) => {
- if (response.data.type === command.id) {
- command.callback(response.data);
- }
- };
- this.dataService.listenOn(command.id, cb);
- this.listeners.push(cb);
+ // reset loading state when content resource changes
+ effect(() => {
+ if (this.contentItems() !== undefined) {
+ this.isLoadingMore.set(false);
}
});
+
+ this.playlistService.getPortalFavorites().subscribe((favorites) => {
+ favorites.forEach((fav: any) => {
+ this.favorites.set(fav.id, true);
+ });
+ });
}
handshake() {
- this.sendRequest({
+ /* this.sendRequest({
action: StalkerPortalActions.Handshake,
type: ContentType.STB,
- });
- }
-
- setInitialBreadcrumb(action: StalkerPortalActions, title: string) {
- this.breadcrumbs = [{ title, action }];
- }
-
- getCategories(contentType: ContentType) {
- this.router.navigate([], { queryParams: { type: contentType } });
- this.selectedContentType = contentType;
- this.currentLayout = 'category';
- const action = StalkerContentTypes[contentType].getCategoryAction;
- this.pageIndex = 0;
- this.setInitialBreadcrumb(
- action,
- this.translate.instant('PORTALS.ALL_CATEGORIES')
- );
- this.sendRequest({ action, type: contentType });
- }
-
- getOrderedList(type: ContentType, category: string, movieId?: string) {
- if (!movieId) {
- this.currentLayout = 'category_content';
- } else {
- if (type === ContentType.SERIES) {
- this.currentLayout = 'serial-details';
- } else if (type === ContentType.VODS) {
- this.currentLayout = 'vod-details';
- }
- }
- const action = StalkerPortalActions.GetOrderedList;
- this.setInitialBreadcrumb(
- action,
- this.translate.instant('PORTALS.ALL_CATEGORIES')
- );
- this.sendRequest({
- action,
- type,
- category,
- ...(movieId ? { movie_id: movieId } : {}),
- });
- }
-
- sendRequest(params: Record) {
- if (params.action !== StalkerPortalActions.CreateLink) {
- this.isLoading = true;
- this.items = [];
- }
- const { portalUrl, macAddress } = this.currentPlaylist();
- let token = {};
- if (sessionStorage.getItem(this.currentPlaylist()._id)) {
- token = sessionStorage.getItem(this.currentPlaylist()._id);
- }
-
- this.updateRoute(params);
-
- this.dataService.sendIpcEvent(STALKER_REQUEST, {
- url: portalUrl,
- macAddress,
- params: {
- ...params,
- token,
- },
- });
- }
-
- updateRoute(params: Record) {
- let queryParams: Params;
- if (params.action === StalkerPortalActions.GetCategories) {
- queryParams = {
- action: params.action,
- type: params.type,
- };
- } else if (params.action === StalkerPortalActions.GetOrderedList) {
- queryParams = {
- action: params.action,
- type: params.type,
- ...(params.category ? { category: params.category } : {}),
- ...(params.movie_id ? { movie_id: params.movie_id } : {}),
- };
- }
- this.router.navigate([], {
- queryParams,
- });
- }
-
- showErrorAsNotification(response: { message: string; status: number }) {
- this.snackBar.open(
- `Error: ${response?.message ?? 'Something went wrong'} (Status: ${
- response?.status ?? 0
- })`,
- null,
- { duration: 4000 }
- );
- this.isLoading = false;
- }
-
- handleResponse(response: {
- action: StalkerPortalActions;
- payload: { js: any; cmd?: string };
- }) {
- if (typeof response.payload !== 'object') {
- this.isLoading = false;
- return;
- }
-
- if (
- this.currentLayout === 'serial-details' &&
- response.action !== StalkerPortalActions.CreateLink
- ) {
- this.seasons = response.payload.js.data;
- this.portalStore.setCurrentSerial(this.seasons);
- }
- if (
- response.action === StalkerPortalActions.GetCategories ||
- response.action === StalkerPortalActions.GetGenres
- ) {
- this.items = response.payload.js.map((item) => ({
- category_name: item.title,
- category_id: item.id,
- }));
- } else if (response.action === StalkerPortalActions.GetOrderedList) {
- if (response.payload.js.data && response.payload.js.data.length > 0)
- this.portalStore.setContent(response.payload.js.data);
-
- this.items = response.payload.js.data.map((item) => ({
- ...item,
- cover: item.screenshot_uri,
- }));
- this.length = response.payload.js.total_items;
- } else if (response.action === StalkerPortalActions.CreateLink) {
- let url = response.payload.js.cmd as string;
- if (url?.startsWith('ffmpeg')) {
- url = url.split(' ')[1];
- }
-
- this.openPlayer(url);
- } else if (response.action === StalkerPortalActions.Handshake) {
- const token = response.payload.js.token;
- sessionStorage.setItem(this.currentPlaylist()._id, token);
-
- this.sendRequest({
- action: StalkerPortalActions.DoAuth,
- login: this.currentPlaylist().username,
- password: this.currentPlaylist().password,
- type: ContentType.STB,
- token,
- });
- } else if (response.action === StalkerPortalActions.DoAuth) {
- this.getCategories(this.selectedContentType);
- }
-
- this.isLoading = false;
- }
-
- openPlayer(streamUrl: string) {
- // Get fresh settings directly from storage instead of using signal
- this.storage.get(STORE_KEY.Settings).subscribe((settings: Settings) => {
- const player = settings?.player ?? VideoPlayer.VideoJs;
- if (player === VideoPlayer.MPV) {
- if (!this.hideExternalInfoDialog())
- this.dialog.open(ExternalPlayerInfoDialogComponent);
- this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
- url: streamUrl,
- mpvPlayerPath: settings?.mpvPlayerPath,
- });
- } else if (player === VideoPlayer.VLC) {
- if (!this.hideExternalInfoDialog())
- this.dialog.open(ExternalPlayerInfoDialogComponent);
- this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, {
- url: streamUrl,
- vlcPlayerPath: settings?.vlcPlayerPath,
- });
- } else {
- this.dialog.open(
- PlayerDialogComponent,
- {
- data: {
- streamUrl,
- title: this.itvTitle,
- },
- width: '80%',
- }
- );
- }
- });
+ }); */
}
categoryClicked(item: { category_name: string; category_id: string }) {
+ this.stalkerStore.setSelectedCategory(Number(item.category_id));
this.currentLayout = 'category_content';
- this.currentCategoryId = item.category_id;
- const action =
- StalkerContentTypes[this.selectedContentType].getContentAction;
- this.portalStore.setSearchPhrase('');
+ this.stalkerStore.setPage(0);
+ this.stalkerStore.setSelectedItem(undefined);
+ if (this.stalkerStore.selectedContentType() === 'itv') return;
- this.breadcrumbs.push({
- title: item.category_name,
- category_id: item.category_id,
- action,
- });
-
- this.sendRequest({
- action,
- type: this.selectedContentType,
- category: item.category_id,
- genre: item.category_id,
+ this.router.navigate(['.', item.category_id], {
+ relativeTo: this.activatedRoute,
});
}
- favoriteClicked(item: StalkerFavoriteItem) {
- if (item.movie_id) {
- this.getSerialDetails(item);
- } else if (item.stream_id && item.details) {
- this.itemDetails = item.details;
- this.breadcrumbs.push({
- title: this.itemDetails?.info?.name,
- action: StalkerPortalActions.GetOrderedList,
+ createLinkToPlayItv(item: any) {
+ this.stalkerStore.setSelectedItem(item);
+ this.stalkerStore.createLinkToPlayVod(item.cmd, item.name, item.logo);
+ }
+
+ toggleFavorite(item: any) {
+ if (this.favorites.has(item.id)) {
+ this.stalkerStore.removeFromFavorites(item.id);
+ this.favorites.delete(item.id);
+ this.snackBar.open('Removed from favorites', null, {
+ duration: 1000,
});
- this.currentLayout = 'vod-details';
} else {
- this.snackBar.open('Something went wrong, id is missing.');
- }
- }
-
- getSerialDetails(item: StalkerFavoriteItem) {
- this.sendRequest({
- action: StalkerPortalActions.GetOrderedList,
- type: ContentType.SERIES,
- movie_id: item.id ?? item.movie_id,
- });
- this.breadcrumbs.push({
- title: item.name,
- action: StalkerPortalActions.GetCategories,
- });
- this.currentLayout = 'serial-details';
- }
-
- getVodDetails(item: {
- id: string;
- cmd: string;
- category_id: string;
- movie_id?: string;
- details: any;
- name: string;
- }) {
- const selectedContent = this.portalStore.getContentById(item.id)();
- this.itemDetails = {
- id: item.id,
- cmd: selectedContent.cmd,
- info: {
- movie_image: selectedContent.screenshot_uri,
- description: selectedContent.description,
- name: selectedContent.name,
- director: selectedContent.director,
- releasedate: selectedContent.year,
- genre: selectedContent.genres_str,
- actors: selectedContent.actors,
- rating_imdb: selectedContent.rating_imdb,
- rating_kinopoisk: selectedContent.rating_kinopoisk,
- },
- };
- this.breadcrumbs.push({
- title: this.itemDetails?.info?.name,
- action: StalkerPortalActions.GetOrderedList,
- });
- this.currentLayout = 'vod-details';
- }
-
- itemClicked(item: {
- id: string;
- cmd: string;
- category_id: string;
- movie_id?: string;
- details: any;
- name: string;
- }) {
- this.itvTitle = item.name;
- if (this.selectedContentType === ContentType.SERIES) {
- this.getSerialDetails(item);
- } else if (this.selectedContentType === ContentType.ITV) {
- this.createLinkToPlayVod(item.cmd);
- } else if (this.selectedContentType === ContentType.VODS) {
- this.getVodDetails(item);
- }
- }
-
- createLinkToPlayEpisode(payload: StalkerSeason) {
- this.sendRequest({
- action: StalkerPortalActions.CreateLink,
- type: ContentType.VODS,
- cmd: payload.cmd,
- series: payload.series,
- forced_storage: 'undefined',
- disable_ad: '0',
- JsHttpRequest: '1-xml',
- });
- }
-
- createLinkToPlayVod(cmd?: string) {
- this.sendRequest({
- action: StalkerPortalActions.CreateLink,
- type: this.selectedContentType,
- cmd: cmd ?? this.itemDetails.cmd,
- forced_storage: 'undefined',
- disable_ad: '0',
- JsHttpRequest: '1-xml',
- });
- }
-
- breadcrumbClicked(breadcrumb: Breadcrumb) {
- const itemIndex = this.breadcrumbs.findIndex((i) => i === breadcrumb);
-
- // do nothing if last breadcrumb child was clicked
- if (itemIndex === this.breadcrumbs.length - 1) return;
-
- this.breadcrumbs.splice(
- itemIndex + 1,
- this.breadcrumbs.length - itemIndex - 1
- );
-
- if (breadcrumb.action === StalkerPortalActions.GetOrderedList) {
- this.currentLayout = 'category_content';
- } else if (
- breadcrumb.action === StalkerPortalActions.GetCategories ||
- breadcrumb.action === StalkerPortalActions.GetGenres
- ) {
- this.pageIndex = 0;
- this.currentLayout = 'category';
- } else if (breadcrumb.action === StalkerPortalActions.Favorites) {
- this.currentLayout = 'favorites';
- }
-
- this.currentCategoryId = breadcrumb.category_id;
- this.sendRequest({
- action: breadcrumb.action,
- type: this.selectedContentType,
- ...(breadcrumb.category_id
- ? {
- category: breadcrumb.category_id,
- genre: breadcrumb.category_id,
- }
- : {}),
- });
- }
-
- addToFavorites(item: any) {
- this.playlistService
- .addPortalFavorite(this.currentPlaylist()._id, item)
- .subscribe(() => {
- this.snackBar.open(
- this.translate.instant('PORTALS.ADDED_TO_FAVORITES'),
- null,
- {
- duration: 1000,
- }
- );
+ this.stalkerStore.addToFavorites({
+ ...item,
+ category_id: 'itv',
+ title: item.name,
+ cover: item.logo,
+ added_at: new Date().toISOString(),
});
- }
-
- removeFromFavorites(favoriteId: string) {
- this.playlistService
- .removeFromPortalFavorites(this.currentPlaylist()._id, favoriteId)
- .subscribe(() => {
- this.snackBar.open(
- this.translate.instant('PORTALS.REMOVED_FROM_FAVORITES'),
- null,
- {
- duration: 1000,
- }
- );
- });
- }
-
- setSearchPhrase(searchPhrase: string) {
- if (
- this.currentLayout === 'category_content' &&
- this.searchPhrase !== searchPhrase
- ) {
- this.pageIndex = 0;
- this.searchPhrase = searchPhrase;
- this.sendRequest({
- action: StalkerContentTypes[this.selectedContentType]
- .getContentAction,
- type: this.selectedContentType,
- search: searchPhrase,
- ...(this.currentCategoryId
- ? {
- category: this.currentCategoryId,
- genre: this.currentCategoryId,
- }
- : {}),
+ this.favorites.set(item.id, true);
+ this.snackBar.open('Added to favorites', null, {
+ duration: 1000,
});
}
}
- handlePageChange(event: PageEvent) {
- this.pageIndex = Number(event.pageIndex);
- this.sendRequest({
- action: StalkerPortalActions.GetOrderedList,
- type: this.selectedContentType,
- category: this.currentCategoryId,
- genre: this.currentCategoryId,
- p: this.pageIndex + 1,
- });
- }
-
- favoriteViewClicked() {
- this.currentLayout = 'favorites';
- this.setInitialBreadcrumb(
- StalkerPortalActions.Favorites,
- this.translate.instant('PORTALS.MY_FAVORITES')
- );
- }
-
- ngOnDestroy() {
- this.portalStore.setSearchPhrase('');
- if (this.dataService.isElectron) {
- this.commandsList.forEach((command) =>
- this.dataService.removeAllListeners(command.id)
- );
- } else {
- this.listeners.forEach((listener) => {
- window.removeEventListener('message', listener);
- });
+ loadMore() {
+ if (this.isLoadingMore() || !this.hasMoreItems()) {
+ return;
}
+
+ this.isLoadingMore.set(true);
+ const nextPage = this.pageIndex() + 1;
+ this.stalkerStore.setPage(nextPage);
+ /* this.pageIndex.set(nextPage); */
+ }
+
+ historyBack() {
+ this.currentLayout = 'category_content';
+ }
+
+ backToCategories() {
+ this.stalkerStore.setSelectedCategory(null);
}
}
diff --git a/src/app/stalker/stalker-search/stalker-search.component.html b/src/app/stalker/stalker-search/stalker-search.component.html
new file mode 100644
index 000000000..9575c0531
--- /dev/null
+++ b/src/app/stalker/stalker-search/stalker-search.component.html
@@ -0,0 +1,78 @@
+
+
+
+ @if (itemDetails) {
+
+ @switch (selectedFilterType()) {
+ @case ('vod') {
+
+ }
+ @case ('series') {
+
+ }
+ }
+ } @else {
+
+
+ @if (searchResultsResource.isLoading()) {
+
+ } @else {
+
+ @for (
+ item of searchResultsResource.value();
+ track item.id
+ ) {
+
+ }
+
+ @if (searchResultsResource.value().length === 0) {
+
+ }
+ }
+
+ }
+
diff --git a/src/app/stalker/stalker-search/stalker-search.component.ts b/src/app/stalker/stalker-search/stalker-search.component.ts
new file mode 100644
index 000000000..dc0398a46
--- /dev/null
+++ b/src/app/stalker/stalker-search/stalker-search.component.ts
@@ -0,0 +1,166 @@
+import { Component, inject, resource, signal } from '@angular/core';
+import { MatButton } from '@angular/material/button';
+import { MatCardModule } from '@angular/material/card';
+import { MatIcon } from '@angular/material/icon';
+import { MatProgressSpinner } from '@angular/material/progress-spinner';
+import { ActivatedRoute } from '@angular/router';
+import { Store } from '@ngrx/store';
+import { TranslatePipe } from '@ngx-translate/core';
+import { STALKER_REQUEST } from '../../../../shared/ipc-commands';
+import { StalkerPortalActions } from '../../../../shared/stalker-portal-actions.enum';
+import { DataService } from '../../services/data.service';
+import { SearchFormComponent } from '../../shared/components/search-form/search-form.component';
+import { SearchResultItemComponent } from '../../shared/components/search-result-item/search-result-item.component';
+import { selectPlaylistById } from '../../state/selectors';
+import { PlaylistErrorViewComponent } from '../../xtream-tauri/playlist-error-view/playlist-error-view.component';
+import { VodDetailsComponent } from '../../xtream/vod-details/vod-details.component';
+import { StalkerContentTypes } from '../stalker-content-types';
+import { StalkerSeriesViewComponent } from '../stalker-series-view/stalker-series-view.component';
+import { StalkerStore } from '../stalker.store';
+
+@Component({
+ selector: 'app-stalker-search',
+ imports: [
+ MatCardModule,
+ MatButton,
+ MatIcon,
+ MatProgressSpinner,
+ PlaylistErrorViewComponent,
+ SearchFormComponent,
+ TranslatePipe,
+ SearchResultItemComponent,
+ StalkerSeriesViewComponent,
+ VodDetailsComponent,
+ ],
+ templateUrl: './stalker-search.component.html',
+ styles: `
+ :host {
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ height: 100%;
+ width: 100%;
+ }
+
+ .search-page {
+ padding: 20px;
+ }
+
+ .results-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: 1rem;
+ padding-bottom: 1rem;
+ }
+ `,
+})
+export class StalkerSearchComponent {
+ private readonly activatedRoute = inject(ActivatedRoute);
+ private readonly dataService = inject(DataService);
+ private readonly stalkerStore = inject(StalkerStore);
+ private readonly store = inject(Store);
+
+ readonly filters = {
+ series: false,
+ vod: true,
+ };
+
+ readonly filterConfig = [
+ {
+ key: 'vod',
+ label: 'Movies',
+ translationKey: 'PORTALS.SIDEBAR.MOVIES',
+ },
+ {
+ key: 'series',
+ label: 'Series',
+ translationKey: 'PORTALS.SIDEBAR.SERIES',
+ },
+ ];
+
+ readonly searchTerm = signal('');
+
+ private readonly currentPlaylist = this.store.selectSignal(
+ selectPlaylistById(this.activatedRoute?.parent?.snapshot.params.id)
+ );
+
+ readonly selectedFilterType = signal('vod');
+
+ itemDetails = null;
+
+ readonly searchResultsResource = resource({
+ request: () => ({
+ contentType: this.selectedFilterType(),
+ search: this.searchTerm(),
+ action: StalkerPortalActions.GetOrderedList,
+ }),
+ loader: async ({ request }) => {
+ if (request.search.length < 3) {
+ return [];
+ }
+ const { portalUrl, macAddress } = this.currentPlaylist();
+
+ const response = await this.dataService.sendIpcEvent(
+ STALKER_REQUEST,
+ {
+ url: portalUrl,
+ macAddress,
+ params: {
+ action: StalkerContentTypes[request.contentType]
+ .getContentAction,
+ type: request.contentType,
+ search: request.search,
+ max_page_items: 100,
+ },
+ }
+ );
+ if (response) {
+ return response.js?.data;
+ } else {
+ throw new Error(
+ `Error: ${response.message} (Status: ${response.status})`
+ );
+ }
+ },
+ });
+
+ createLinkToPlayVodItv(cmd?: string, title?: string, thumbnail?: string) {
+ this.stalkerStore.createLinkToPlayVod(cmd, title, thumbnail);
+ }
+
+ selectItem(item: any) {
+ this.itemDetails = {
+ ...item,
+ info: { ...item, movie_image: item.screenshot_uri },
+ };
+ this.stalkerStore.setSelectedItem(this.itemDetails);
+
+ switch (this.selectedFilterType()) {
+ case 'vod':
+ this.stalkerStore.setSelectedContentType('vod');
+ break;
+ case 'series':
+ this.stalkerStore.setSelectedContentType('series');
+ break;
+ default:
+ break;
+ }
+ }
+
+ onFiltersChange(event: { vod: boolean; live: boolean; series: boolean }) {
+ const selectedFilter = Object.keys(event).find((key) => event[key]);
+ if (selectedFilter) {
+ this.selectedFilterType.set(selectedFilter);
+ }
+ }
+
+ addToFavorites(item: any) {
+ console.debug('Add to favorites', item);
+ this.stalkerStore.addToFavorites(item);
+ }
+
+ removeFromFavorites(favoriteId: string) {
+ console.debug('Remove from favorites', favoriteId);
+ this.stalkerStore.removeFromFavorites(favoriteId);
+ }
+}
diff --git a/src/app/stalker/stalker-series-view/stalker-series-view.component.html b/src/app/stalker/stalker-series-view/stalker-series-view.component.html
index 684a808db..92f488505 100644
--- a/src/app/stalker/stalker-series-view/stalker-series-view.component.html
+++ b/src/app/stalker/stalker-series-view/stalker-series-view.component.html
@@ -1,65 +1,58 @@
+@let seasons = seasonsData;
- @if (seasons()[0]; as serial) {
+ @if (selectedItem(); as serial) {
-
![]()
-
+ @if (serial.info.movie_image) {
+
+ } @else {
-
+ }
-
{{ serial.name }}
+
{{ serial.info.name }}
- {{ serial.description }}
+ {{ serial.info.description }}
{{ 'XTREAM.DIRECTOR' | translate }}:
- {{ serial.director }}
+ {{ serial.info.director }}
{{ 'XTREAM.ACTORS' | translate }}:
- {{ serial.actors }}
+ {{ serial.info.actors }}
{{ 'XTREAM.YEAR' | translate }}:
- {{ serial.year }}
+ {{ serial.info.releasedate }}
{{ 'XTREAM.GENRE' | translate }}:
- {{ serial.genres_str }}
-
-
-
{{ 'XTREAM.AGE' | translate }}:
- {{ serial.age }}
+ {{ serial.info.genre }}
{{ 'XTREAM.IMDB_RATING' | translate }}:
- {{ serial.rating_imdb }}
+ {{ serial.info.rating_imdb }}
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
- {{ serial.rating_kinopoisk }}
-
-
-
- {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
-
- {{ serial.added }}
+ {{ serial.info.rating_kinopoisk }}
@@ -73,9 +66,7 @@
@for (episode of item.series; track $index) {
diff --git a/src/app/stalker/stalker-series-view/stalker-series-view.component.ts b/src/app/stalker/stalker-series-view/stalker-series-view.component.ts
index ee9664f4a..7e694cf7a 100644
--- a/src/app/stalker/stalker-series-view/stalker-series-view.component.ts
+++ b/src/app/stalker/stalker-series-view/stalker-series-view.component.ts
@@ -1,46 +1,44 @@
-import { NgIf, NgOptimizedImage } from '@angular/common';
-import { Component, Signal, input, output } from '@angular/core';
-import { MatButtonModule } from '@angular/material/button';
-import { MatDividerModule } from '@angular/material/divider';
-import { TranslateModule } from '@ngx-translate/core';
+import { Component, inject } from '@angular/core';
+import { MatButton } from '@angular/material/button';
+import { MatDivider } from '@angular/material/divider';
+import { TranslatePipe } from '@ngx-translate/core';
import { FavoritesButtonComponent } from '../favorites-button/favorites-button.component';
-import { StalkerSeason } from '../models';
-
-function sortByNumericValue(array: StalkerSeason[]): StalkerSeason[] {
- if (!array) return [];
- const key = 'name';
- return array.sort((a, b) => {
- const numericA = extractNumericValue(a[key]);
- const numericB = extractNumericValue(b[key]);
- return numericA - numericB;
- });
-}
-
-function extractNumericValue(str: string) {
- const matches = str.match(/\d+/);
- if (matches) {
- return parseInt(matches[0], 10);
- }
- return 0;
-}
+import { StalkerStore } from '../stalker.store';
@Component({
selector: 'app-stalker-series-view',
templateUrl: './stalker-series-view.component.html',
styleUrls: ['../../xtream/detail-view.scss'],
- imports: [
- FavoritesButtonComponent,
- MatButtonModule,
- MatDividerModule,
- NgIf,
- NgOptimizedImage,
- TranslateModule,
- ],
+ imports: [FavoritesButtonComponent, MatButton, MatDivider, TranslatePipe],
})
export class StalkerSeriesViewComponent {
- seasons: Signal
= input.required({
- transform: sortByNumericValue,
- });
+ readonly stalkerStore = inject(StalkerStore);
- playEpisode = output<{ series: string; cmd: string }>();
+ readonly selectedItem = this.stalkerStore.selectedItem;
+ readonly seasonsData = this.stalkerStore.getSerialSeasonsResource;
+
+ playEpisodeClicked(episode: any, cmd: string) {
+ const item = this.selectedItem();
+ this.stalkerStore.createLinkToPlayVod(
+ cmd,
+ item.info.name,
+ item.info.movie_image,
+ episode
+ );
+ }
+
+ addToFavorites(item: any) {
+ this.stalkerStore.addToFavorites({
+ ...item,
+ title: item.info.name,
+ cover: item.info.movie_image,
+ series_id: item.id,
+ added_at: new Date().toISOString(),
+ category_id: 'series',
+ });
+ }
+
+ removeFromFavorites(serialId: string) {
+ this.stalkerStore.removeFromFavorites(serialId);
+ }
}
diff --git a/src/app/stalker/stalker-shell/stalker-shell.component.html b/src/app/stalker/stalker-shell/stalker-shell.component.html
new file mode 100644
index 000000000..35f6aeaa9
--- /dev/null
+++ b/src/app/stalker/stalker-shell/stalker-shell.component.html
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/stalker/stalker-shell/stalker-shell.component.scss b/src/app/stalker/stalker-shell/stalker-shell.component.scss
new file mode 100644
index 000000000..39846aa78
--- /dev/null
+++ b/src/app/stalker/stalker-shell/stalker-shell.component.scss
@@ -0,0 +1,18 @@
+:host {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ height: 100%;
+}
+
+.stalker-container {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+ min-height: 0;
+}
+
+.navigation-sidebar {
+ width: 200px;
+ border-right: 1px solid #333;
+}
diff --git a/src/app/stalker/stalker-shell/stalker-shell.component.ts b/src/app/stalker/stalker-shell/stalker-shell.component.ts
new file mode 100644
index 000000000..39003fe67
--- /dev/null
+++ b/src/app/stalker/stalker-shell/stalker-shell.component.ts
@@ -0,0 +1,74 @@
+import { Component, effect, inject, OnDestroy } from '@angular/core';
+import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
+import { Store } from '@ngrx/store';
+import * as PlaylistActions from '../../state/actions';
+import { selectPlaylistById } from '../../state/selectors';
+import { NavigationComponent } from '../../xtream-tauri/navigation/navigation.component';
+import { NavigationItem } from '../../xtream-tauri/navigation/navigation.interface';
+import { StalkerStore } from '../stalker.store';
+
+@Component({
+ selector: 'app-stalker-shell',
+ templateUrl: './stalker-shell.component.html',
+ styleUrls: ['./stalker-shell.component.scss'],
+ standalone: true,
+ imports: [NavigationComponent, RouterOutlet, NavigationComponent],
+})
+export class StalkerShellComponent implements OnDestroy {
+ private readonly route = inject(ActivatedRoute);
+ private readonly router = inject(Router);
+ readonly stalkerStore = inject(StalkerStore);
+ private readonly store = inject(Store);
+
+ readonly mainNavigationItems: NavigationItem[] = [
+ {
+ id: 'vod',
+ icon: 'movie',
+ labelKey: 'PORTALS.SIDEBAR.MOVIES',
+ },
+ {
+ id: 'itv',
+ icon: 'live_tv',
+ labelKey: 'PORTALS.SIDEBAR.LIVE_TV',
+ },
+ {
+ id: 'series',
+ icon: 'tv',
+ labelKey: 'PORTALS.SIDEBAR.SERIES',
+ },
+ ];
+
+ readonly currentPlaylist = this.store.selectSignal(
+ selectPlaylistById(this.route.snapshot.params.id)
+ );
+
+ constructor() {
+ this.store.dispatch(
+ PlaylistActions.setCurrentPlaylistId({
+ playlistId: this.route.snapshot.params['id'],
+ })
+ );
+ this.stalkerStore.setSelectedContentType('vod');
+ this.stalkerStore.setSelectedCategory(null);
+ effect(() => {
+ this.stalkerStore.setCurrentPlaylist(this.currentPlaylist());
+ });
+ }
+
+ setContentType(type: 'vod' | 'live' | 'series' | 'itv') {
+ if (type === 'live') type = 'itv';
+ this.stalkerStore.setSelectedContentType(type);
+ this.stalkerStore.setSelectedCategory(null);
+ this.router.navigate([type], {
+ relativeTo: this.route,
+ });
+ }
+
+ handlePageClick() {
+ this.stalkerStore.setSelectedContentType(null);
+ }
+
+ ngOnDestroy() {
+ this.stalkerStore.resetCategories();
+ }
+}
diff --git a/src/app/stalker/stalker.routes.ts b/src/app/stalker/stalker.routes.ts
new file mode 100644
index 000000000..cdfd76733
--- /dev/null
+++ b/src/app/stalker/stalker.routes.ts
@@ -0,0 +1,92 @@
+import { Route } from '@angular/router';
+
+export const stalkerRoutes: Route[] = [
+ {
+ path: 'stalker/:id',
+ loadComponent: () =>
+ import('./stalker-shell/stalker-shell.component').then(
+ (c) => c.StalkerShellComponent
+ ),
+ children: [
+ {
+ path: '',
+ redirectTo: 'vod',
+ pathMatch: 'full',
+ },
+ {
+ path: 'vod',
+ loadComponent: () =>
+ import('./stalker-main-container.component').then(
+ (c) => c.StalkerMainContainerComponent
+ ),
+ children: [
+ {
+ path: ':categoryId',
+ data: {
+ api: 'stalker',
+ contentType: 'vod',
+ },
+ loadComponent: () =>
+ import(
+ '../xtream-tauri/category-content-view/category-content-view.component'
+ ).then((c) => c.CategoryContentViewComponent),
+ },
+ ],
+ },
+
+ {
+ path: '',
+ redirectTo: 'vod',
+ pathMatch: 'full',
+ },
+ {
+ path: 'itv',
+ loadComponent: () =>
+ import('./stalker-main-container.component').then(
+ (c) => c.StalkerMainContainerComponent
+ ),
+ },
+ {
+ path: 'series',
+ loadComponent: () =>
+ import('./stalker-main-container.component').then(
+ (c) => c.StalkerMainContainerComponent
+ ),
+ children: [
+ {
+ path: ':categoryId',
+ loadComponent: () =>
+ import(
+ '../xtream-tauri/category-content-view/category-content-view.component'
+ ).then((c) => c.CategoryContentViewComponent),
+ data: {
+ api: 'stalker',
+ contentType: 'series',
+ },
+ },
+ ],
+ },
+ {
+ path: 'favorites',
+ loadComponent: () =>
+ import(
+ './stalker-favorites/stalker-favorites.component'
+ ).then((c) => c.StalkerFavoritesComponent),
+ },
+ {
+ path: 'recent',
+ loadComponent: () =>
+ import('./recently-viewed/recently-viewed.component').then(
+ (c) => c.RecentlyViewedComponent
+ ),
+ },
+ {
+ path: 'search',
+ loadComponent: () =>
+ import('./stalker-search/stalker-search.component').then(
+ (c) => c.StalkerSearchComponent
+ ),
+ },
+ ],
+ },
+];
diff --git a/src/app/stalker/stalker.store.ts b/src/app/stalker/stalker.store.ts
new file mode 100644
index 000000000..e42bb9ba6
--- /dev/null
+++ b/src/app/stalker/stalker.store.ts
@@ -0,0 +1,502 @@
+import { computed, inject, resource, signal } from '@angular/core';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import {
+ patchState,
+ signalStore,
+ withComputed,
+ withMethods,
+ withProps,
+ withState,
+} from '@ngrx/signals';
+import { TranslateService } from '@ngx-translate/core';
+import { STALKER_REQUEST } from '../../../shared/ipc-commands';
+import { StalkerPortalActions } from '../../../shared/stalker-portal-actions.enum';
+import { DataService } from '../services/data.service';
+import { PlayerService } from '../services/player.service';
+import { PlaylistsService } from '../services/playlists.service';
+import { PlaylistMeta } from '../shared/playlist-meta.type';
+import { ContentType } from '../xtream/content-type.enum';
+import { StalkerSeason } from './models/stalker-season.interface';
+import { StalkerContentTypes } from './stalker-content-types';
+
+interface StalkerCategoryItem {
+ category_id: string;
+ category_name: string;
+}
+
+interface StalkerState {
+ selectedContentType: 'vod' | 'itv' | 'series';
+ selectedCategoryId: string;
+ selectedVodId: string;
+ selectedSerialId: string;
+ selectedItvId: string;
+ limit: number;
+ page: number;
+ searchPhrase: string;
+ currentPlaylist: PlaylistMeta;
+ totalCount: number;
+ selectedItem: any;
+ vodCategories: StalkerCategoryItem[];
+ seriesCategories: StalkerCategoryItem[];
+ itvCategories: StalkerCategoryItem[];
+ hasMoreChannels: boolean;
+ itvChannels: any[];
+}
+
+const initialState: StalkerState = {
+ selectedContentType: 'vod',
+ selectedCategoryId: undefined,
+ selectedVodId: undefined,
+ selectedSerialId: undefined,
+ selectedItvId: undefined,
+ limit: 14,
+ page: 0,
+ searchPhrase: '',
+ currentPlaylist: undefined,
+ totalCount: 0,
+ selectedItem: undefined,
+ vodCategories: [],
+ seriesCategories: [],
+ itvCategories: [],
+ hasMoreChannels: false,
+ itvChannels: [],
+};
+
+function extractNumericValue(str: string) {
+ const matches = str.match(/\d+/);
+ if (matches) {
+ return parseInt(matches[0], 10);
+ }
+ return 0;
+}
+
+function sortByNumericValue(array: StalkerSeason[]): StalkerSeason[] {
+ if (!array) return [];
+ const key = 'name';
+ return array.sort((a, b) => {
+ const numericA = extractNumericValue(a[key]);
+ const numericB = extractNumericValue(b[key]);
+ return numericA - numericB;
+ });
+}
+
+export const StalkerStore = signalStore(
+ { providedIn: 'root' },
+ withState(initialState),
+ withProps((store, dataService = inject(DataService)) => ({
+ getCategoryResource: resource({
+ request: () => ({
+ contentType: store.selectedContentType(),
+ action: StalkerPortalActions.GetCategories,
+ currentPlaylist: store.currentPlaylist(),
+ }),
+ loader: async ({ request }) => {
+ if (request.currentPlaylist === undefined) return;
+
+ switch (request.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;
+ }
+
+ const { portalUrl, macAddress } = request.currentPlaylist;
+
+ const response = await dataService.sendIpcEvent(
+ STALKER_REQUEST,
+ {
+ url: portalUrl,
+ macAddress,
+ params: {
+ action: StalkerContentTypes[request.contentType]
+ .getCategoryAction,
+ type: request.contentType,
+ },
+ }
+ );
+ if (response) {
+ const categories = response.js
+ .map((item) => ({
+ category_name: item.title,
+ category_id: item.id,
+ }))
+ .sort((a, b) =>
+ a.category_name.localeCompare(b.category_name)
+ );
+ patchState(store, {
+ [`${request.contentType}Categories`]: categories,
+ });
+ return categories;
+ } else {
+ throw new Error(
+ `Error: ${response.message} (Status: ${response.status})`
+ );
+ }
+ },
+ }),
+ getContentResource: resource({
+ request: () => ({
+ contentType: store.selectedContentType(),
+ category: store.selectedCategoryId(),
+ action: StalkerPortalActions.GetOrderedList,
+ search: store.searchPhrase(),
+ pageIndex: store.page() + 1,
+ }),
+ loader: async ({ request }) => {
+ if (
+ !request.category ||
+ request.category === null ||
+ request.category === ''
+ ) {
+ return Promise.resolve(undefined);
+ }
+
+ const currentPlaylist = store.currentPlaylist;
+ const params = {
+ action: StalkerContentTypes[request.contentType]
+ .getContentAction,
+ type: request.contentType,
+ category: request.category ?? '',
+ genre: request.category ?? '',
+ sortby: 'added',
+ ...(request.search !== ''
+ ? { search: request.search }
+ : {}),
+ p: request.pageIndex,
+ };
+
+ const response = await dataService.sendIpcEvent(
+ STALKER_REQUEST,
+ {
+ url: currentPlaylist().portalUrl,
+ macAddress: currentPlaylist().macAddress,
+ params,
+ }
+ );
+
+ patchState(store, { totalCount: response.js.total_items });
+
+ if (response) {
+ const newItems = response.js.data.map((item) => ({
+ ...item,
+ cover: item.screenshot_uri,
+ }));
+
+ if (store.selectedContentType() === 'itv') {
+ // Check if we're loading the first page or loading more
+ if (request.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;
+ patchState(store, {
+ hasMoreChannels:
+ totalLoaded < response.js.total_items,
+ });
+ }
+
+ return newItems;
+ } else {
+ throw new Error(
+ `Error: ${response.message} (Status: ${response.status})`
+ );
+ }
+ },
+ }),
+ serialSeasonsResource: resource({
+ request: () => ({
+ itemId: store.selectedSerialId(),
+ }),
+ loader: async ({ request }) => {
+ const { portalUrl, macAddress } = store.currentPlaylist();
+ const params = {
+ action: StalkerContentTypes.series.getContentAction,
+ type: 'series',
+ movie_id: request.itemId,
+ };
+ const response = await dataService.sendIpcEvent(
+ STALKER_REQUEST,
+ {
+ url: portalUrl,
+ macAddress,
+ params,
+ }
+ );
+ return sortByNumericValue(response.js.data);
+ },
+ }),
+ })),
+ withComputed((store) => ({
+ getTotalPages: computed(() => {
+ return Math.ceil(store.totalCount() / store.limit());
+ }),
+ /** category content */
+ getPaginatedContent: computed(() => store.getContentResource.value()),
+ isPaginatedContentLoading: computed(() =>
+ store.getContentResource.isLoading()
+ ),
+ isPaginatedContentFailed: computed(() =>
+ store.getContentResource.error()
+ ),
+ /** serials */
+ getSerialSeasonsResource: computed(() =>
+ store.serialSeasonsResource.value()
+ ),
+ /** category resource */
+ getCategoryResource: computed(() => store.getCategoryResource.value()),
+ isCategoryResourceLoading: computed(() =>
+ store.getCategoryResource.isLoading()
+ ),
+ isCategoryResourceFailed: computed(() =>
+ store.getCategoryResource.error()
+ ),
+ getSelectedCategoryName: computed(() => {
+ const type = store.selectedContentType();
+ const selectedCategoryId = store.selectedCategoryId();
+ if (!selectedCategoryId) return '';
+ let categories = [];
+ if (type === 'vod') {
+ categories = store.vodCategories();
+ } else if (type === 'series') {
+ categories = store.seriesCategories();
+ } else if (type === 'itv') {
+ categories = store.itvCategories();
+ }
+ const category = categories.find(
+ (cat) => String(cat.category_id) === String(selectedCategoryId)
+ );
+ return category ? category.category_name : '';
+ }),
+ })),
+ withMethods(
+ (
+ store,
+ dataService = inject(DataService),
+ playerService = inject(PlayerService),
+ playlistService = inject(PlaylistsService),
+ snackBar = inject(MatSnackBar),
+ translate = inject(TranslateService)
+ ) => ({
+ /** selectors */
+ setSelectedContentType(type: 'vod' | 'itv' | 'series') {
+ patchState(store, { selectedContentType: type });
+ },
+ setSelectedCategory(id: number) {
+ patchState(store, {
+ selectedCategoryId: id !== null ? String(id) : null,
+ page: 0,
+ });
+ },
+ setSelectedSerialId(id: string) {
+ patchState(store, { selectedSerialId: id });
+ },
+ setSelectedVodId(id: string) {
+ patchState(store, { selectedVodId: id });
+ },
+ setSelectedItvId(id: string) {
+ patchState(store, { selectedItvId: id });
+ },
+ setLimit(limit: number) {
+ patchState(store, { limit });
+ },
+ setPage(page: number) {
+ patchState(store, { page });
+ },
+ setCurrentPlaylist(playlist: PlaylistMeta | undefined) {
+ patchState(store, { currentPlaylist: playlist });
+ },
+ setSelectedItem(selectedItem: any) {
+ // TODO: check the item type and proper property either selectedVodId or serialsId etc
+ patchState(store, {
+ selectedVodId: selectedItem?.id ?? undefined,
+ selectedSerialId: selectedItem?.id ?? undefined,
+ selectedItvId: selectedItem?.id ?? undefined,
+ selectedItem,
+ });
+ },
+ setCategories(type: 'vod' | 'series' | 'itv', categories: any[]) {
+ if (type === 'vod') {
+ patchState(store, { vodCategories: categories });
+ } else if (type === 'series') {
+ patchState(store, { seriesCategories: categories });
+ } else if (type === 'itv') {
+ patchState(store, { itvCategories: categories });
+ }
+ },
+ resetCategories() {
+ patchState(store, {
+ vodCategories: [],
+ seriesCategories: [],
+ itvCategories: [],
+ });
+ },
+ setItvChannels(channels: any[]) {
+ patchState(store, { itvChannels: channels });
+ },
+ /** getters */
+ getSelectedCategory() {
+ return signal(store.selectedCategoryId());
+ },
+ /** API */
+ async fetchLinkToPlay(
+ portalUrl: string,
+ macAddress: string,
+ cmd: string,
+ series?: number
+ ) {
+ const type = series
+ ? ContentType.VODS
+ : store.selectedContentType();
+ const params = {
+ action: StalkerContentTypes[store.selectedContentType()]
+ .getLink,
+ type,
+ cmd,
+ forced_storage: 'undefined',
+ disable_ad: '0',
+ JsHttpRequest: '1-xml',
+ ...(series ? { series } : {}),
+ };
+ const response = await dataService.sendIpcEvent(
+ STALKER_REQUEST,
+ {
+ url: portalUrl + '?' + cmd,
+ macAddress,
+ params,
+ }
+ );
+ let url = response.js.cmd as string;
+ if (url?.startsWith('ffmpeg')) {
+ url = url.split(' ')[1];
+ }
+ return url;
+ },
+ async getExpireDate() {
+ const params = {
+ type: 'account_info',
+ action: 'get_main_info',
+ JsHttpRequest: '1-xml',
+ };
+
+ try {
+ const response = await dataService.sendIpcEvent(
+ STALKER_REQUEST,
+ {
+ url: store.currentPlaylist().portalUrl,
+ macAddress: store.currentPlaylist().macAddress,
+ params,
+ }
+ );
+
+ if (response && response.js && response.js.account_info) {
+ // Extract the expire date from the response
+ const expireDate = response.js.account_info.expire_date;
+
+ // Convert timestamp to readable date if it's a unix timestamp
+ if (expireDate && !isNaN(expireDate)) {
+ const date = new Date(expireDate * 1000); // Convert seconds to milliseconds
+ return date.toLocaleDateString();
+ }
+
+ return expireDate || 'Unknown';
+ }
+
+ return 'Unknown';
+ } catch (error) {
+ console.error('Failed to fetch expire date:', error);
+ return 'Error fetching data';
+ }
+ },
+ addToFavorites(item: any) {
+ playlistService
+ .addPortalFavorite(this.currentPlaylist()?._id, {
+ ...item,
+ category_id: store.selectedContentType(),
+ added_at: Date.now(),
+ id: item.stream_id ?? item.id,
+ })
+ .subscribe(() => {
+ snackBar.open(
+ translate.instant('PORTALS.ADDED_TO_FAVORITES'),
+ null,
+ {
+ duration: 1000,
+ }
+ );
+ });
+ },
+ removeFromFavorites(favoriteId: string) {
+ playlistService
+ .removeFromPortalFavorites(
+ this.currentPlaylist()?._id,
+ favoriteId
+ )
+ .subscribe(() => {
+ snackBar.open(
+ translate.instant('PORTALS.REMOVED_FROM_FAVORITES'),
+ null,
+ {
+ duration: 1000,
+ }
+ );
+ });
+ },
+ async createLinkToPlayVod(
+ cmd?: string,
+ title?: string,
+ thumbnail?: string,
+ episode?: number
+ ) {
+ const url = await this.fetchLinkToPlay(
+ this.currentPlaylist().portalUrl,
+ this.currentPlaylist().macAddress,
+ cmd ?? this.selectedItem().cmd,
+ episode
+ );
+ const item = this.selectedItem();
+ this.addToRecentlyViewed({
+ ...item,
+ id: item.id,
+ cmd: cmd,
+ cover: thumbnail,
+ title,
+ });
+ playerService.openPlayer(url, title, thumbnail);
+ },
+ addToRecentlyViewed(item: any) {
+ console.log('Adding to recently viewed', item);
+ playlistService
+ .addPortalRecentlyViewed({
+ ...item,
+ category_id: store.selectedContentType(),
+ added_at: Date.now(),
+ })
+ .subscribe();
+ },
+ removeFromRecentlyViewed(itemId: number) {
+ playlistService
+ .removeFromPortalRecentlyViewed(itemId)
+ .subscribe();
+ },
+ })
+ )
+);