mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
feat(stalker): rework stalker portal UI
- Added `StalkerSearchComponent` for searching VOD and series content. - Integrated search form and results grid with loading states. - Implemented item selection and details display for VOD and series. - Created a new `StalkerShellComponent` for navigation and routing. - Established routing for Stalker module with nested routes for VOD, series, and favorites. - Introduced a centralized `StalkerStore` for managing state and API interactions. - Enhanced `StalkerSeriesViewComponent` to display detailed information about selected series. - Updated styles for improved layout and user experience.
This commit is contained in:
1 parent
4759f62f71
commit
87807dc1e7
23 files changed
+1752
-824
No files matched your search
@@ -34,4 +34,5 @@ export interface Playlist {
|
||||
password?: string;
|
||||
macAddress?: string;
|
||||
portalUrl?: string;
|
||||
recentlyViewed?: any[];
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { stalkerRoutes } from './stalker/stalker.routes';
|
||||
import { xtreamRoutes } from './xtream-tauri/xtream.routes';
|
||||
import { AppConfig } from '../environments/environment';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -56,6 +56,7 @@ const routes: Routes = [
|
||||
(c) => c.StalkerMainContainerComponent
|
||||
),
|
||||
},
|
||||
...stalkerRoutes,
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '',
|
||||
@@ -64,9 +65,11 @@ const routes: Routes = [
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
RouterModule.forRoot(
|
||||
routes /* {
|
||||
enableTracing: !AppConfig.production,
|
||||
}),
|
||||
} */
|
||||
),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
|
||||
@@ -211,7 +211,7 @@ export class RecentPlaylistsComponent implements OnDestroy {
|
||||
if (playlistMeta.serverUrl) {
|
||||
this.router.navigate(['xtreams', playlistMeta._id]);
|
||||
} else if (playlistMeta.macAddress) {
|
||||
this.router.navigate(['portals', playlistMeta._id]);
|
||||
this.router.navigate(['stalker', playlistMeta._id]);
|
||||
} else {
|
||||
this.router.navigate(['playlists', playlistMeta._id]);
|
||||
this.playlistClicked.emit(playlistMeta._id);
|
||||
|
||||
@@ -56,6 +56,7 @@ export class StalkerPortalImportComponent {
|
||||
Validators.pattern(this.URL_REGEX),
|
||||
]),
|
||||
importDate: new FormControl(new Date().toISOString()),
|
||||
userAgent: new FormControl(''),
|
||||
});
|
||||
|
||||
dataService = inject(DataService);
|
||||
|
||||
@@ -48,6 +48,11 @@ export const dbConfig: DBConfig = {
|
||||
keypath: 'favorites',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'recentlyViewed',
|
||||
keypath: 'recentlyViewed',
|
||||
options: { unique: false },
|
||||
},
|
||||
{
|
||||
name: 'autoRefresh',
|
||||
keypath: 'autoRefresh',
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
<button
|
||||
*ngIf="!isFavorite; else removeFromFavoritesButton"
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
(click)="addToFavorites()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
|
||||
<ng-template #removeFromFavoritesButton>
|
||||
@let isFavorite = isFavorite$ | async;
|
||||
@if (isFavorite === false) {
|
||||
<button mat-flat-button color="accent" (click)="addToFavorites()">
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-stroked-button color="accent" (click)="removeFromFavorites()">
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
</ng-template>
|
||||
}
|
||||
@@ -1,83 +1,48 @@
|
||||
import { NgIf } from '@angular/common';
|
||||
import { Component, OnInit, inject, input } from '@angular/core';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { Component, inject, input, output } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BehaviorSubject, map, switchMap } from 'rxjs';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-favorites-button',
|
||||
templateUrl: './favorites-button.component.html',
|
||||
imports: [MatButtonModule, MatIconModule, NgIf, TranslateModule]
|
||||
imports: [AsyncPipe, MatButtonModule, MatIconModule, TranslateModule],
|
||||
})
|
||||
export class FavoritesButtonComponent implements OnInit {
|
||||
export class FavoritesButtonComponent {
|
||||
private playlistService = inject(PlaylistsService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
portalId = this.route.snapshot.paramMap.get('id');
|
||||
serialMeta = input.required<{
|
||||
movie_id: string;
|
||||
name: string;
|
||||
cover: string;
|
||||
}>();
|
||||
readonly itemId = input.required<string>();
|
||||
|
||||
isFavorite = false;
|
||||
readonly addToFavoritesClicked = output<void>();
|
||||
readonly removeFromFavoritesClicked = output<void>();
|
||||
|
||||
ngOnInit() {
|
||||
this.checkFavoriteStatus();
|
||||
}
|
||||
private readonly favoritesChanged$ = new BehaviorSubject<void>(undefined);
|
||||
|
||||
checkFavoriteStatus() {
|
||||
this.playlistService
|
||||
.getPortalFavorites(this.portalId)
|
||||
.subscribe((favorites) => {
|
||||
this.isFavorite = favorites.some(
|
||||
(i) => (i as any).movie_id === this.serialMeta().movie_id
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
toggleFavorites(isFav: boolean) {
|
||||
if (isFav) this.removeFromFavorites();
|
||||
else this.addToFavorites();
|
||||
}
|
||||
readonly isFavorite$ = this.favoritesChanged$.pipe(
|
||||
switchMap(() => this.playlistService.getPortalFavorites()),
|
||||
map((favorites) =>
|
||||
favorites.some(
|
||||
(i) =>
|
||||
(i as any).movie_id === this.itemId() ||
|
||||
(i as any).id === this.itemId()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
removeFromFavorites() {
|
||||
this.playlistService
|
||||
.removeFromPortalFavorites(
|
||||
this.portalId,
|
||||
this.serialMeta().movie_id
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.snackBar.open(
|
||||
this.translateService.instant(
|
||||
'PORTALS.REMOVED_FROM_FAVORITES'
|
||||
),
|
||||
null,
|
||||
{
|
||||
duration: 1000,
|
||||
}
|
||||
);
|
||||
this.checkFavoriteStatus();
|
||||
});
|
||||
this.removeFromFavoritesClicked.emit();
|
||||
setTimeout(() => {
|
||||
this.favoritesChanged$.next();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
addToFavorites() {
|
||||
this.playlistService
|
||||
.addPortalFavorite(this.portalId, this.serialMeta())
|
||||
.subscribe(() => {
|
||||
this.snackBar.open(
|
||||
this.translateService.instant('PORTALS.ADDED_TO_FAVORITES'),
|
||||
null,
|
||||
{
|
||||
duration: 1000,
|
||||
}
|
||||
);
|
||||
this.checkFavoriteStatus();
|
||||
});
|
||||
this.addToFavoritesClicked.emit();
|
||||
setTimeout(() => {
|
||||
this.favoritesChanged$.next();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
@if (itemDetails) {
|
||||
<button
|
||||
mat-button
|
||||
(click)="itemDetails = null"
|
||||
[attr.aria-label]="'SETTINGS.BACK_TO_HOME' | translate"
|
||||
class="back-button"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'SETTINGS.BACK_TO_HOME' | translate }}
|
||||
</button>
|
||||
@switch (itemDetails.category_id) {
|
||||
@case ('vod') {
|
||||
<app-vod-details
|
||||
[item]="itemDetails"
|
||||
(playClicked)="
|
||||
createLinkToPlayVodItv(
|
||||
itemDetails.cmd,
|
||||
itemDetails.info.name,
|
||||
itemDetails.info.movie_image
|
||||
)
|
||||
"
|
||||
(addToFavoritesClicked)="addToFavorites($event)"
|
||||
(removeFromFavoritesClicked)="removeFromFavorites($event)"
|
||||
/>
|
||||
}
|
||||
@case ('series') {
|
||||
<app-stalker-series-view />
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<app-favorites-layout
|
||||
[categories]="categories() ?? []"
|
||||
[favorites]="itemsToShow() ?? []"
|
||||
[selectedCategoryId]="selectedCategoryId()"
|
||||
(categoryClicked)="setCategoryId($event.category_id)"
|
||||
(removeFromFavorites)="removeFromRecentlyViewed($event)"
|
||||
[titleTranslationString]="'PORTALS.SIDEBAR.RECENT'"
|
||||
(openItem)="openItem($event)"
|
||||
/>
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { rxResource } from '@angular/core/rxjs-interop';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
import { FavoritesLayoutComponent } from '../../shared/components/favorites-layout/favorites-layout.component';
|
||||
import { VodDetailsComponent } from '../../xtream/vod-details/vod-details.component';
|
||||
import { StalkerSeriesViewComponent } from '../stalker-series-view/stalker-series-view.component';
|
||||
import { StalkerStore } from '../stalker.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-recently-viewed',
|
||||
templateUrl: './recently-viewed.component.html',
|
||||
imports: [
|
||||
FavoritesLayoutComponent,
|
||||
MatButton,
|
||||
MatIcon,
|
||||
StalkerSeriesViewComponent,
|
||||
TranslatePipe,
|
||||
VodDetailsComponent,
|
||||
],
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin: 10px 0 0 16px;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class RecentlyViewedComponent {
|
||||
private readonly playlistService = inject(PlaylistsService);
|
||||
private readonly refreshTimestamp = signal<number>(Date.now());
|
||||
private readonly stalkerStore = inject(StalkerStore);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
itemDetails: any = null;
|
||||
|
||||
readonly allFavorites = rxResource({
|
||||
request: () => ({
|
||||
refreshTimestamp: this.refreshTimestamp(),
|
||||
}),
|
||||
loader: () => {
|
||||
return this.playlistService.getPortalRecentlyViewed();
|
||||
},
|
||||
});
|
||||
|
||||
readonly categories = computed(() => [
|
||||
{
|
||||
id: 1,
|
||||
category_id: 'movie',
|
||||
category_name:
|
||||
this.translate.instant('PORTALS.SIDEBAR.MOVIES') +
|
||||
' (' +
|
||||
(this.movies()?.length ?? 0) +
|
||||
')',
|
||||
parent_id: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category_id: 'itv',
|
||||
category_name:
|
||||
this.translate.instant('PORTALS.SIDEBAR.LIVE_TV') +
|
||||
' (' +
|
||||
(this.live()?.length ?? 0) +
|
||||
')',
|
||||
parent_id: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category_id: 'series',
|
||||
category_name:
|
||||
this.translate.instant('PORTALS.SIDEBAR.SERIES') +
|
||||
' (' +
|
||||
(this.series()?.length ?? 0) +
|
||||
')',
|
||||
parent_id: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
readonly itemsToShow = computed(() => {
|
||||
switch (this.selectedCategoryId()) {
|
||||
case 'movie':
|
||||
return this.movies();
|
||||
case 'itv':
|
||||
return this.live();
|
||||
case 'series':
|
||||
return this.series();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
readonly selectedCategoryId = signal<string>('movie');
|
||||
|
||||
readonly series = computed(() =>
|
||||
this.allFavorites
|
||||
.value()
|
||||
?.filter((item) => item.category_id === 'series')
|
||||
);
|
||||
readonly movies = computed(() =>
|
||||
this.allFavorites.value()?.filter((item) => item.category_id === 'vod')
|
||||
);
|
||||
readonly live = computed(() =>
|
||||
this.allFavorites.value()?.filter((item) => item.category_id === 'itv')
|
||||
);
|
||||
|
||||
removeFromRecentlyViewed(item: any) {
|
||||
this.stalkerStore.removeFromRecentlyViewed(item.id);
|
||||
setTimeout(() => {
|
||||
this.refreshTimestamp.set(Date.now());
|
||||
}, 100);
|
||||
}
|
||||
|
||||
setCategoryId(categoryId: any) {
|
||||
this.selectedCategoryId.set(categoryId);
|
||||
}
|
||||
|
||||
openItem(item: any) {
|
||||
this.stalkerStore.setSelectedContentType(item.category_id);
|
||||
switch (item.category_id) {
|
||||
case 'itv':
|
||||
this.createLinkToPlayVodItv(item.cmd, item.name, item.logo);
|
||||
break;
|
||||
case 'vod':
|
||||
case 'series':
|
||||
this.itemDetails = item;
|
||||
this.stalkerStore.setSelectedItem(item);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async createLinkToPlayVodItv(
|
||||
cmd?: string,
|
||||
title?: string,
|
||||
thumbnail?: string
|
||||
) {
|
||||
console.debug('Create link to play VOD/ITV', cmd, title, thumbnail);
|
||||
await this.stalkerStore.createLinkToPlayVod(cmd, title, thumbnail);
|
||||
}
|
||||
|
||||
addToFavorites(item: any) {
|
||||
this.stalkerStore.addToFavorites(item);
|
||||
}
|
||||
|
||||
removeFromFavorites(favoriteId: string) {
|
||||
this.stalkerStore.removeFromFavorites(favoriteId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
@if (itemDetails) {
|
||||
<button
|
||||
mat-button
|
||||
(click)="itemDetails = null"
|
||||
[attr.aria-label]="'SETTINGS.BACK_TO_HOME' | translate"
|
||||
class="back-button"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'SETTINGS.BACK_TO_HOME' | translate }}
|
||||
</button>
|
||||
@switch (itemDetails.category_id) {
|
||||
@case ('vod') {
|
||||
<app-vod-details
|
||||
[item]="itemDetails.details"
|
||||
(playClicked)="
|
||||
createLinkToPlayVodItv(
|
||||
itemDetails.details.cmd,
|
||||
itemDetails.details.info.name,
|
||||
itemDetails.details.info.movie_image
|
||||
)
|
||||
"
|
||||
/>
|
||||
}
|
||||
@case ('series') {
|
||||
<app-stalker-series-view />
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<app-favorites-layout
|
||||
[categories]="categories() ?? []"
|
||||
[favorites]="favoritesToShow() ?? []"
|
||||
[selectedCategoryId]="selectedCategoryId()"
|
||||
(categoryClicked)="setCategoryId($event.category_id)"
|
||||
(removeFromFavorites)="removeFromFavorites($event)"
|
||||
(openItem)="openItem($event)"
|
||||
/>
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Component, computed, inject, signal } from '@angular/core';
|
||||
import { rxResource } from '@angular/core/rxjs-interop';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistsService } from '../../services/playlists.service';
|
||||
import { FavoritesLayoutComponent } from '../../shared/components/favorites-layout/favorites-layout.component';
|
||||
import { VodDetailsComponent } from '../../xtream/vod-details/vod-details.component';
|
||||
import { StalkerSeriesViewComponent } from '../stalker-series-view/stalker-series-view.component';
|
||||
import { StalkerStore } from '../stalker.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stalker-favorites',
|
||||
templateUrl: './stalker-favorites.component.html',
|
||||
imports: [
|
||||
FavoritesLayoutComponent,
|
||||
MatButton,
|
||||
MatIcon,
|
||||
StalkerSeriesViewComponent,
|
||||
TranslatePipe,
|
||||
VodDetailsComponent,
|
||||
],
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin: 10px 0 0 16px;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class StalkerFavoritesComponent {
|
||||
private readonly playlistService = inject(PlaylistsService);
|
||||
private readonly refreshTimestamp = signal<number>(Date.now());
|
||||
private readonly stalkerStore = inject(StalkerStore);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
itemDetails: any = null;
|
||||
|
||||
readonly allFavorites = rxResource({
|
||||
request: () => ({
|
||||
refreshTimestamp: this.refreshTimestamp(),
|
||||
}),
|
||||
loader: () => {
|
||||
return this.playlistService.getPortalFavorites();
|
||||
},
|
||||
});
|
||||
|
||||
readonly categories = computed(() => [
|
||||
{
|
||||
id: 1,
|
||||
category_id: 'movie',
|
||||
category_name:
|
||||
this.translate.instant('PORTALS.SIDEBAR.MOVIES') +
|
||||
' (' +
|
||||
(this.movies()?.length ?? 0) +
|
||||
')',
|
||||
parent_id: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
category_id: 'itv',
|
||||
category_name:
|
||||
this.translate.instant('PORTALS.SIDEBAR.LIVE_TV') +
|
||||
' (' +
|
||||
(this.live()?.length ?? 0) +
|
||||
')',
|
||||
parent_id: 0,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
category_id: 'series',
|
||||
category_name:
|
||||
this.translate.instant('PORTALS.SIDEBAR.SERIES') +
|
||||
' (' +
|
||||
(this.series()?.length ?? 0) +
|
||||
')',
|
||||
parent_id: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
readonly favoritesToShow = computed(() => {
|
||||
switch (this.selectedCategoryId()) {
|
||||
case 'movie':
|
||||
return this.movies();
|
||||
case 'itv':
|
||||
return this.live();
|
||||
case 'series':
|
||||
return this.series();
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
readonly selectedCategoryId = signal<string>('movie');
|
||||
|
||||
readonly series = computed(() =>
|
||||
this.allFavorites
|
||||
.value()
|
||||
?.filter((item) => item.category_id === 'series')
|
||||
);
|
||||
readonly movies = computed(() =>
|
||||
this.allFavorites.value()?.filter((item) => item.category_id === 'vod')
|
||||
);
|
||||
readonly live = computed(() =>
|
||||
this.allFavorites.value()?.filter((item) => item.category_id === 'itv')
|
||||
);
|
||||
|
||||
removeFromFavorites(item: any) {
|
||||
this.stalkerStore.removeFromFavorites(item.id);
|
||||
setTimeout(() => {
|
||||
this.refreshTimestamp.set(Date.now());
|
||||
}, 100);
|
||||
}
|
||||
|
||||
setCategoryId(categoryId: any) {
|
||||
this.selectedCategoryId.set(categoryId);
|
||||
}
|
||||
|
||||
openItem(item: any) {
|
||||
console.debug('Open item', item);
|
||||
switch (item.category_id) {
|
||||
case 'itv':
|
||||
this.stalkerStore.setSelectedContentType('itv');
|
||||
this.createLinkToPlayVodItv(item.cmd, item.name, item.logo);
|
||||
break;
|
||||
case 'vod':
|
||||
this.itemDetails = item;
|
||||
this.stalkerStore.setSelectedItem(item);
|
||||
this.stalkerStore.setSelectedContentType('vod');
|
||||
break;
|
||||
case 'series':
|
||||
this.itemDetails = item;
|
||||
|
||||
this.stalkerStore.setSelectedItem(item);
|
||||
this.stalkerStore.setSelectedContentType('series');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async createLinkToPlayVodItv(
|
||||
cmd?: string,
|
||||
title?: string,
|
||||
thumbnail?: string
|
||||
) {
|
||||
console.debug('Create link to play VOD/ITV', cmd, title, thumbnail);
|
||||
await this.stalkerStore.createLinkToPlayVod(cmd, title, thumbnail);
|
||||
}
|
||||
}
|
||||
@@ -1,99 +1,120 @@
|
||||
<app-navigation-bar
|
||||
[breadcrumbs]="breadcrumbs"
|
||||
[contentType]="selectedContentType"
|
||||
(searchTextChanged)="setSearchPhrase($event)"
|
||||
[clientSideSearch]="currentLayout === 'category_content' ? false : true"
|
||||
[searchVisible]="
|
||||
(currentLayout === 'category' ||
|
||||
currentLayout === 'category_content') &&
|
||||
items?.length > 0
|
||||
"
|
||||
[contentTypeNavigationItems]="navigationContentTypes"
|
||||
[contentType]="selectedContentType"
|
||||
(contentTypeChanged)="getCategories($event)"
|
||||
(breadcrumbClicked)="breadcrumbClicked($event)"
|
||||
(favoritesClicked)="favoriteViewClicked()"
|
||||
[showCategories]="true"
|
||||
/>
|
||||
|
||||
@if (isLoading) {
|
||||
<div class="loading-wrapper">
|
||||
@if (currentLayout === 'category_content') {
|
||||
<div class="loading-container category-content-view">
|
||||
@for (a of [].constructor(14); track $index) {
|
||||
<div class="skeleton-item">
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '170px', height: '270px' }"
|
||||
/>
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '60%', height: '30px' }"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (currentLayout === 'category') {
|
||||
<div class="loading-container category-view">
|
||||
@for (a of [].constructor(14); track $index) {
|
||||
<ngx-skeleton-loader
|
||||
count="1"
|
||||
[theme]="{ width: '170px', height: '51px' }"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div class="sidebar" style="display: flex; flex-direction: column">
|
||||
<div class="sidebar-header">
|
||||
@let isItvCategorySelected =
|
||||
stalkerStore.selectedCategoryId() &&
|
||||
stalkerStore.selectedContentType() === 'itv';
|
||||
@if (isItvCategorySelected) {
|
||||
<button mat-icon-button (click)="backToCategories()">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<h2>{{ selectedCategoryTitle() }}</h2>
|
||||
} @else {
|
||||
<button mat-icon-button disabled>
|
||||
<mat-icon>list</mat-icon>
|
||||
</button>
|
||||
<h2>{{ 'PORTALS.ALL_CATEGORIES' | translate }}</h2>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
@switch (currentLayout) {
|
||||
@case ('category') {
|
||||
<app-category-view
|
||||
(categoryClicked)="categoryClicked($event)"
|
||||
[items]="items"
|
||||
/>
|
||||
}
|
||||
@case ('category_content') {
|
||||
<app-category-content-view
|
||||
[items]="items"
|
||||
(itemClicked)="itemClicked($event)"
|
||||
/>
|
||||
@if (currentLayout === 'category_content' && length && length > 0) {
|
||||
<mat-paginator
|
||||
[length]="length"
|
||||
[pageIndex]="pageIndex"
|
||||
[pageSize]="pageSize"
|
||||
(page)="handlePageChange($event)"
|
||||
aria-label="Select page"
|
||||
/>
|
||||
@if (isItvCategorySelected) {
|
||||
<mat-nav-list style="overflow: auto; flex: 1">
|
||||
@for (item of itvChannels(); track $index) {
|
||||
<mat-list-item (click)="createLinkToPlayItv(item)">
|
||||
<img
|
||||
matListItemAvatar
|
||||
class="stream-icon"
|
||||
[src]="item.logo"
|
||||
(error)="
|
||||
$event.target.src =
|
||||
'./assets/images/default-poster.png'
|
||||
"
|
||||
/>
|
||||
{{ item.name }}
|
||||
<button
|
||||
mat-icon-button
|
||||
matListItemMeta
|
||||
(click)="toggleFavorite(item); $event.stopPropagation()"
|
||||
>
|
||||
<mat-icon>{{
|
||||
favorites.get(item.id) ? 'star' : 'star_outline'
|
||||
}}</mat-icon>
|
||||
</button>
|
||||
</mat-list-item>
|
||||
}
|
||||
</mat-nav-list>
|
||||
|
||||
@if (hasMoreItems() && !isContentLoading()) {
|
||||
<mat-divider />
|
||||
<div>
|
||||
<button
|
||||
mat-button
|
||||
(click)="loadMore()"
|
||||
[disabled]="isLoadingMore()"
|
||||
style="width: 100%"
|
||||
>
|
||||
@if (isLoadingMore()) {
|
||||
Loading page {{ pageIndex() + 1 }}...
|
||||
} @else {
|
||||
Load More (Page {{ pageIndex() + 1 }})
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@case ('vod-details') {
|
||||
<app-vod-details
|
||||
[item]="itemDetails"
|
||||
(playClicked)="createLinkToPlayVod(itemDetails.cmd)"
|
||||
(addToFavoritesClicked)="addToFavorites($event)"
|
||||
(removeFromFavoritesClicked)="removeFromFavorites($event)"
|
||||
} @else {
|
||||
@if (isCategoryLoading()) {
|
||||
<ngx-skeleton-loader
|
||||
count="20"
|
||||
[theme]="{
|
||||
width: '95%',
|
||||
height: '48px',
|
||||
borderRadius: '40px',
|
||||
}"
|
||||
/>
|
||||
}
|
||||
@case ('serial-details') {
|
||||
<app-stalker-series-view
|
||||
[seasons]="seasons"
|
||||
(playEpisode)="createLinkToPlayEpisode($event)"
|
||||
/>
|
||||
}
|
||||
@case ('favorites') {
|
||||
<app-category-content-view
|
||||
[items]="favorites$ | async"
|
||||
(itemClicked)="favoriteClicked($event)"
|
||||
/>
|
||||
}
|
||||
@case ('not-available') {
|
||||
} @else if (isCategoryFailed()) {
|
||||
<app-playlist-error-view
|
||||
[title]="'PORTALS.ERROR_VIEW.ACCOUNT_EXPIRED.TITLE' | translate"
|
||||
[title]="'PORTALS.ERROR_VIEW.UNKNOWN_ERROR.TITLE' | translate"
|
||||
[description]="
|
||||
'PORTALS.ERROR_VIEW.ACCOUNT_EXPIRED.DESCRIPTION' | translate
|
||||
'PORTALS.ERROR_VIEW.UNKNOWN_ERROR.DESCRIPTION' | translate
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<app-category-view
|
||||
[items]="categories()"
|
||||
[selectedCategoryId]="stalkerStore.selectedCategoryId()"
|
||||
(categoryClicked)="categoryClicked($event)"
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="content-header">
|
||||
<h2>
|
||||
@if (
|
||||
currentLayout === 'vod-details' ||
|
||||
currentLayout === 'serial-details'
|
||||
) {
|
||||
<button mat-icon-button (click)="historyBack()">
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (!isItvCategorySelected) {
|
||||
<button mat-icon-button disabled>
|
||||
<mat-icon>category</mat-icon>
|
||||
</button>
|
||||
{{ selectedCategoryTitle() }}
|
||||
}
|
||||
|
||||
@if (!contentItems() && !isContentLoading()) {
|
||||
<h2>
|
||||
{{ 'PORTALS.SELECT_CATEGORY' | translate }}
|
||||
</h2>
|
||||
}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="scrollable-content">
|
||||
<router-outlet />
|
||||
</div>
|
||||
<app-mpv-player-bar />
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Settings>;
|
||||
favorites$: Observable<any>;
|
||||
| '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<number, boolean>();
|
||||
|
||||
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<string, string | number | string[]>) {
|
||||
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<string, string | number | string[]>) {
|
||||
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, PlayerDialogData>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<div class="search-page">
|
||||
<div class="header">
|
||||
<h2>{{ 'PORTALS.SIDEBAR.SEARCH' | translate }}</h2>
|
||||
</div>
|
||||
|
||||
@if (itemDetails) {
|
||||
<button
|
||||
mat-button
|
||||
(click)="itemDetails = null"
|
||||
[attr.aria-label]="'SETTINGS.BACK_TO_HOME' | translate"
|
||||
class="back-button"
|
||||
>
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
{{ 'SETTINGS.BACK_TO_HOME' | translate }}
|
||||
</button>
|
||||
@switch (selectedFilterType()) {
|
||||
@case ('vod') {
|
||||
<app-vod-details
|
||||
[item]="itemDetails"
|
||||
(playClicked)="
|
||||
createLinkToPlayVodItv(
|
||||
itemDetails.cmd,
|
||||
itemDetails.info.name,
|
||||
itemDetails.info.movie_image
|
||||
)
|
||||
"
|
||||
(addToFavoritesClicked)="addToFavorites($event)"
|
||||
(removeFromFavoritesClicked)="removeFromFavorites($event)"
|
||||
/>
|
||||
}
|
||||
@case ('series') {
|
||||
<app-stalker-series-view />
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<app-search-form
|
||||
[singleSelection]="true"
|
||||
[(searchTerm)]="searchTerm"
|
||||
[filters]="filters"
|
||||
[filterConfig]="filterConfig"
|
||||
(filtersChange)="onFiltersChange($event)"
|
||||
/>
|
||||
<div class="results-container">
|
||||
@if (searchResultsResource.isLoading()) {
|
||||
<mat-spinner />
|
||||
} @else {
|
||||
<div class="results-grid">
|
||||
@for (
|
||||
item of searchResultsResource.value();
|
||||
track item.id
|
||||
) {
|
||||
<app-search-result-item
|
||||
[title]="item.name"
|
||||
[posterUrl]="item.screenshot_uri"
|
||||
[type]="item.type"
|
||||
(itemClick)="selectItem(item)"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@if (searchResultsResource.value().length === 0) {
|
||||
<div class="no-results">
|
||||
<app-playlist-error-view
|
||||
[title]="
|
||||
'PORTALS.EMPTY_LIST_VIEW.TITLE' | translate
|
||||
"
|
||||
[description]="
|
||||
'PORTALS.EMPTY_LIST_VIEW.NO_SEARCH_RESULTS'
|
||||
| translate
|
||||
"
|
||||
[showActionButtons]="false"
|
||||
[viewType]="'NO_SEARCH_RESULTS'"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,58 @@
|
||||
@let seasons = seasonsData;
|
||||
<div class="container">
|
||||
@if (seasons()[0]; as serial) {
|
||||
@if (selectedItem(); as serial) {
|
||||
<div class="image">
|
||||
<img
|
||||
*ngIf="serial.screenshot_uri; else placeholderCover"
|
||||
[ngSrc]="serial.screenshot_uri"
|
||||
style="position: relative"
|
||||
fill
|
||||
/>
|
||||
<ng-template #placeholderCover>
|
||||
@if (serial.info.movie_image) {
|
||||
<img
|
||||
[src]="serial.info.movie_image"
|
||||
style="position: relative"
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<div class="placeholder-cover"></div>
|
||||
</ng-template>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<h2>{{ serial.name }}</h2>
|
||||
<h2>{{ serial.info.name }}</h2>
|
||||
<div>
|
||||
{{ serial.description }}
|
||||
{{ serial.info.description }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ serial.director }}
|
||||
{{ serial.info.director }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
|
||||
{{ serial.actors }}
|
||||
{{ serial.info.actors }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.YEAR' | translate }}:</div>
|
||||
{{ serial.year }}
|
||||
{{ serial.info.releasedate }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ serial.genres_str }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.AGE' | translate }}:</div>
|
||||
{{ serial.age }}
|
||||
{{ serial.info.genre }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div>
|
||||
{{ serial.rating_imdb }}
|
||||
{{ serial.info.rating_imdb }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
|
||||
</div>
|
||||
{{ serial.rating_kinopoisk }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">
|
||||
{{ 'HOME.PLAYLISTS.ADDED' | translate }}:
|
||||
</div>
|
||||
{{ serial.added }}
|
||||
{{ serial.info.rating_kinopoisk }}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<app-favorites-button
|
||||
[serialMeta]="{
|
||||
movie_id: serial.id,
|
||||
name: serial.name,
|
||||
cover: serial.screenshot_uri
|
||||
}"
|
||||
[itemId]="serial.id"
|
||||
(addToFavoritesClicked)="addToFavorites(serial)"
|
||||
(removeFromFavoritesClicked)="
|
||||
removeFromFavorites(serial.id)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -73,9 +66,7 @@
|
||||
@for (episode of item.series; track $index) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="
|
||||
playEpisode.emit({ series: episode, cmd: item.cmd })
|
||||
"
|
||||
(click)="playEpisodeClicked(episode, item.cmd)"
|
||||
>
|
||||
{{ 'XTREAM.EPISODE' | translate }} {{ episode }}
|
||||
</button>
|
||||
|
||||
@@ -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<StalkerSeason[]> = 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<div class="stalker-container">
|
||||
<app-navigation
|
||||
class="navigation-sidebar"
|
||||
[navigationItems]="mainNavigationItems"
|
||||
(categoryClick)="setContentType($event)"
|
||||
[selectedContentType]="stalkerStore.selectedContentType()"
|
||||
(pageClicked)="handlePageClick()"
|
||||
/>
|
||||
<router-outlet />
|
||||
</div>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -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<string>(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();
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
Reference in new issue
Block a user