mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-15 21:56:18 -08:00
feat(stalker): support serials api
This commit is contained in:
1 parent
76c1ba9df6
commit
50743f592b
6 files changed
+465
-123
No files matched your search
@@ -10,56 +10,77 @@
|
||||
[contentType]="selectedContentType"
|
||||
(contentTypeChanged)="getCategories($event)"
|
||||
(breadcrumbClicked)="breadcrumbClicked($event)"
|
||||
(favoritesClicked)="favoritesClicked()"
|
||||
(favoritesClicked)="favoriteViewClicked()"
|
||||
[showCategories]="true"
|
||||
/>
|
||||
|
||||
@switch (currentLayout) {
|
||||
@case ('category') {
|
||||
<app-category-view
|
||||
(categoryClicked)="categoryClicked($event)"
|
||||
[items]="items"
|
||||
/>
|
||||
}
|
||||
@case ('category_content') {
|
||||
<app-category-content-view
|
||||
[items]="items"
|
||||
(itemClicked)="itemClicked($event)"
|
||||
/>
|
||||
<mat-paginator
|
||||
*ngIf="currentLayout === 'category_content' && length && length > 0"
|
||||
[length]="length"
|
||||
[pageSize]="pageSize"
|
||||
(page)="handlePageChange($event)"
|
||||
aria-label="Select page"
|
||||
/>
|
||||
}
|
||||
@case ('vod-details') {
|
||||
<app-vod-details
|
||||
[item]="itemDetails"
|
||||
(playClicked)="playVod()"
|
||||
(addToFavoritesClicked)="addToFavorites($event)"
|
||||
(removeFromFavoritesClicked)="removeFromFavorites($event)"
|
||||
/>
|
||||
}
|
||||
@case ('serial-details') {
|
||||
<app-stalker-series-view
|
||||
[seasons]="seasons"
|
||||
(playEpisode)="playEpisode($event)"
|
||||
/>
|
||||
}
|
||||
@case ('favorites') {
|
||||
<app-category-content-view
|
||||
[items]="favorites$ | async"
|
||||
(itemClicked)="itemClicked($event)"
|
||||
/>
|
||||
}
|
||||
@case ('not-available') {
|
||||
<app-playlist-error-view
|
||||
[title]="'PORTALS.ERROR_VIEW.ACCOUNT_EXPIRED.TITLE' | translate"
|
||||
[description]="
|
||||
'PORTALS.ERROR_VIEW.ACCOUNT_EXPIRED.DESCRIPTION' | translate
|
||||
"
|
||||
/>
|
||||
@if (isLoading) {
|
||||
<div class="loading-wrapper">
|
||||
<div class="loading-container">
|
||||
@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>
|
||||
</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)"
|
||||
/>
|
||||
<mat-paginator
|
||||
*ngIf="
|
||||
currentLayout === 'category_content' && length && length > 0
|
||||
"
|
||||
[length]="length"
|
||||
[pageSize]="pageSize"
|
||||
(page)="handlePageChange($event)"
|
||||
aria-label="Select page"
|
||||
/>
|
||||
}
|
||||
@case ('vod-details') {
|
||||
<app-vod-details
|
||||
[item]="itemDetails"
|
||||
(playClicked)="playVod()"
|
||||
(addToFavoritesClicked)="addToFavorites($event)"
|
||||
(removeFromFavoritesClicked)="removeFromFavorites($event)"
|
||||
/>
|
||||
}
|
||||
@case ('serial-details') {
|
||||
<app-stalker-series-view
|
||||
[seasons]="seasons"
|
||||
(playEpisode)="playEpisode($event)"
|
||||
/>
|
||||
}
|
||||
@case ('favorites') {
|
||||
<app-category-content-view
|
||||
[items]="favorites$ | async"
|
||||
(itemClicked)="favoriteClicked($event)"
|
||||
/>
|
||||
}
|
||||
@case ('not-available') {
|
||||
<app-playlist-error-view
|
||||
[title]="'PORTALS.ERROR_VIEW.ACCOUNT_EXPIRED.TITLE' | translate"
|
||||
[description]="
|
||||
'PORTALS.ERROR_VIEW.ACCOUNT_EXPIRED.DESCRIPTION' | translate
|
||||
"
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.loading-wrapper {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
import { AsyncPipe, NgFor, NgIf } 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 { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { Observable } from 'rxjs';
|
||||
import { IpcCommand } from '../../../shared/ipc-command.class';
|
||||
import {
|
||||
@@ -33,20 +35,30 @@ import { PlayerDialogComponent } from '../xtream/player-dialog/player-dialog.com
|
||||
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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-stalker-main-container',
|
||||
templateUrl: './stalker-main-container.component.html',
|
||||
styleUrl: './stalker-main-container.component.scss',
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
CategoryViewComponent,
|
||||
CategoryContentViewComponent,
|
||||
NavigationBarComponent,
|
||||
NgIf,
|
||||
CategoryViewComponent,
|
||||
MatPaginatorModule,
|
||||
NavigationBarComponent,
|
||||
NgFor,
|
||||
NgIf,
|
||||
NgxSkeletonLoaderModule,
|
||||
PlaylistErrorViewComponent,
|
||||
StalkerSeriesViewComponent,
|
||||
TranslateModule,
|
||||
VodDetailsComponent,
|
||||
],
|
||||
@@ -56,11 +68,12 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
|
||||
listeners = [];
|
||||
isLoading = true;
|
||||
selectedContentType: ContentType = ContentType.VODS;
|
||||
selectedContentType = ContentType.VODS;
|
||||
currentLayout:
|
||||
| 'category'
|
||||
| 'category_content'
|
||||
| 'favorites'
|
||||
| 'serial-details'
|
||||
| 'vod-details'
|
||||
| 'not-available' = 'category';
|
||||
searchPhrase = this.portalStore.searchPhrase();
|
||||
@@ -70,19 +83,7 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
favorites$: Observable<any>;
|
||||
|
||||
items: any[] = [];
|
||||
itemDetails!: {
|
||||
id: string;
|
||||
cmd: string;
|
||||
info: {
|
||||
movie_image: string;
|
||||
description: string;
|
||||
name: string;
|
||||
actors: string;
|
||||
director: string;
|
||||
releasedate: string;
|
||||
genre: string;
|
||||
};
|
||||
};
|
||||
itemDetails!: StalkerVodDetails;
|
||||
|
||||
navigationContentTypes: ContentTypeNavigationItem[] = [
|
||||
{
|
||||
@@ -93,6 +94,14 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
contentType: ContentType.VODS,
|
||||
label: 'VOD Streams',
|
||||
},
|
||||
{
|
||||
contentType: ContentType.SERIES,
|
||||
label: 'Series',
|
||||
},
|
||||
/* {
|
||||
contentType: ContentType.RADIO,
|
||||
label: 'Radio',
|
||||
}, */
|
||||
];
|
||||
|
||||
currentCategoryId;
|
||||
@@ -102,6 +111,8 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
pageSize = 14;
|
||||
pageIndex = 0;
|
||||
|
||||
seasons: StalkerSeason[] = [];
|
||||
|
||||
commandsList = [
|
||||
new IpcCommand(STALKER_RESPONSE, (response: any) =>
|
||||
this.handleResponse(response)
|
||||
@@ -111,37 +122,64 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
(response: { message: string; status: number }) => {
|
||||
this.currentLayout = 'not-available';
|
||||
this.showErrorAsNotification(response);
|
||||
this.isLoading = false;
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
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 store: Store,
|
||||
private translate: TranslateService
|
||||
) {
|
||||
effect(() => {
|
||||
if (this.currentPlaylist()) {
|
||||
if (
|
||||
this.currentPlaylist().password &&
|
||||
this.currentPlaylist().username
|
||||
) {
|
||||
this.handshake();
|
||||
} else {
|
||||
this.selectedContentType = ContentType.VODS;
|
||||
this.getCategories(this.selectedContentType);
|
||||
}
|
||||
effect(
|
||||
() => {
|
||||
if (this.currentPlaylist()) {
|
||||
if (
|
||||
this.currentPlaylist().password &&
|
||||
this.currentPlaylist().username
|
||||
) {
|
||||
this.handshake();
|
||||
} else {
|
||||
this.selectedContentType =
|
||||
this.activatedRoute.snapshot.queryParams.type ??
|
||||
ContentType.VODS;
|
||||
|
||||
this.favorites$ = this.playlistService.getPortalFavorites(
|
||||
this.currentPlaylist()._id
|
||||
);
|
||||
}
|
||||
});
|
||||
const action =
|
||||
this.activatedRoute.snapshot.queryParams.action ??
|
||||
'get_categories';
|
||||
const { category, movie_id } =
|
||||
this.activatedRoute.snapshot.queryParams;
|
||||
|
||||
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('');
|
||||
}
|
||||
@@ -165,7 +203,10 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
}
|
||||
|
||||
handshake() {
|
||||
this.sendRequest({ action: 'handshake', type: 'stb' });
|
||||
this.sendRequest({
|
||||
action: StalkerPortalActions.Handshake,
|
||||
type: ContentType.STB,
|
||||
});
|
||||
}
|
||||
|
||||
setInitialBreadcrumb(
|
||||
@@ -176,6 +217,7 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
}
|
||||
|
||||
getCategories(contentType: ContentType = this.selectedContentType) {
|
||||
this.router.navigate([], { queryParams: { type: contentType } });
|
||||
this.selectedContentType = contentType;
|
||||
this.currentLayout = 'category';
|
||||
const action = StalkerContentTypes[contentType].getCategoryAction;
|
||||
@@ -183,14 +225,43 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
this.sendRequest({ action, type: contentType });
|
||||
}
|
||||
|
||||
sendRequest(params: Record<string, string | number>) {
|
||||
this.isLoading = true;
|
||||
if (params.action !== 'create_link') this.items = [];
|
||||
getOrderedList(
|
||||
type: ContentType = this.selectedContentType,
|
||||
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.sendRequest({
|
||||
action,
|
||||
type,
|
||||
category,
|
||||
...(movieId ? { movie_id: movieId } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
sendRequest(params: Record<string, string | number | string[]>) {
|
||||
if (params.action !== 'create_link') {
|
||||
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,
|
||||
@@ -201,6 +272,26 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
updateRoute(params: Record<string, string | number | string[]>) {
|
||||
if (params.action === 'get_categories') {
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
action: params.action,
|
||||
type: params.type,
|
||||
},
|
||||
});
|
||||
} else if (params.action === 'get_ordered_list') {
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
action: params.action,
|
||||
type: params.type,
|
||||
...(params.category ? { category: params.category } : {}),
|
||||
...(params.movie_id ? { movie_id: params.movie_id } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
showErrorAsNotification(response: { message: string; status: number }) {
|
||||
this.snackBar.open(
|
||||
`Error: ${response?.message ?? 'Something went wrong'} (Status: ${
|
||||
@@ -213,14 +304,24 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
}
|
||||
|
||||
handleResponse(response: {
|
||||
action: string;
|
||||
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;
|
||||
}
|
||||
if (
|
||||
response.action === StalkerPortalActions.GetCategories ||
|
||||
response.action === StalkerPortalActions.GetGenres
|
||||
) {
|
||||
if (typeof response.payload !== 'object') return;
|
||||
this.items = response.payload.js.map((item) => ({
|
||||
category_name: item.title,
|
||||
category_id: item.id,
|
||||
@@ -242,20 +343,22 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.openPlayer(url, response.payload.js.name);
|
||||
} else if (response.action === 'handshake') {
|
||||
} else if (response.action === StalkerPortalActions.Handshake) {
|
||||
const token = response.payload.js.token;
|
||||
sessionStorage.setItem(this.currentPlaylist()._id, token);
|
||||
|
||||
this.sendRequest({
|
||||
action: 'do_auth',
|
||||
action: StalkerPortalActions.DoAuth,
|
||||
login: this.currentPlaylist().username,
|
||||
password: this.currentPlaylist().password,
|
||||
type: 'stb',
|
||||
type: ContentType.STB,
|
||||
token,
|
||||
});
|
||||
} else if (response.action === 'do_auth') {
|
||||
} else if (response.action === StalkerPortalActions.DoAuth) {
|
||||
this.getCategories();
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
openPlayer(streamUrl: string, title: string) {
|
||||
@@ -299,27 +402,58 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
itemClicked(i: any) {
|
||||
if (this.selectedContentType === ContentType.ITV) {
|
||||
this.playVod(i.cmd);
|
||||
return;
|
||||
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,
|
||||
});
|
||||
this.currentLayout = 'vod-details';
|
||||
} else {
|
||||
this.snackBar.open('Something went wrong, id is missing.');
|
||||
}
|
||||
const selectedContent = this.portalStore.getContentById(i.id)();
|
||||
this.itemDetails = !selectedContent
|
||||
? i.details // to read from favorites
|
||||
: {
|
||||
id: i.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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -327,6 +461,38 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
this.currentLayout = 'vod-details';
|
||||
}
|
||||
|
||||
itemClicked(item: {
|
||||
id: string;
|
||||
cmd: string;
|
||||
category_id: string;
|
||||
movie_id?: string;
|
||||
details: any;
|
||||
name: string;
|
||||
}) {
|
||||
if (this.selectedContentType === ContentType.SERIES) {
|
||||
this.getSerialDetails(item);
|
||||
return;
|
||||
}
|
||||
if (this.selectedContentType === ContentType.ITV) {
|
||||
this.playVod(item.cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
this.getVodDetails(item);
|
||||
}
|
||||
|
||||
playEpisode(payload: StalkerSeason) {
|
||||
this.sendRequest({
|
||||
action: StalkerContentTypes[ContentType.SERIES].getLink,
|
||||
type: ContentType.VODS,
|
||||
cmd: payload.cmd,
|
||||
series: payload.series,
|
||||
forced_storage: 'undefined',
|
||||
disable_ad: '0',
|
||||
JsHttpRequest: '1-xml',
|
||||
});
|
||||
}
|
||||
|
||||
playVod(cmd?: string) {
|
||||
const command = cmd ?? this.itemDetails.cmd;
|
||||
const action = StalkerContentTypes[this.selectedContentType].getLink;
|
||||
@@ -379,9 +545,13 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
this.playlistService
|
||||
.addPortalFavorite(this.currentPlaylist()._id, item)
|
||||
.subscribe(() => {
|
||||
this.snackBar.open('Added to favorites', null, {
|
||||
duration: 1000,
|
||||
});
|
||||
this.snackBar.open(
|
||||
this.translate.instant('PORTALS.ADDED_TO_FAVORITES'),
|
||||
null,
|
||||
{
|
||||
duration: 1000,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -389,9 +559,13 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
this.playlistService
|
||||
.removeFromPortalFavorites(this.currentPlaylist()._id, favoriteId)
|
||||
.subscribe(() => {
|
||||
this.snackBar.open('Removed from favorites', null, {
|
||||
duration: 1000,
|
||||
});
|
||||
this.snackBar.open(
|
||||
this.translate.instant('PORTALS.REMOVED_FROM_FAVORITES'),
|
||||
null,
|
||||
{
|
||||
duration: 1000,
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -426,15 +600,15 @@ export class StalkerMainContainerComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
favoritesClicked() {
|
||||
favoriteViewClicked() {
|
||||
this.currentLayout = 'favorites';
|
||||
this.setInitialBreadcrumb(
|
||||
StalkerPortalActions.Favorites,
|
||||
'My favorites'
|
||||
this.translate.instant('PORTALS.MY_FAVORITES')
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
ngOnDestroy() {
|
||||
if (this.dataService.isElectron) {
|
||||
this.commandsList.forEach((command) =>
|
||||
this.dataService.removeAllListeners(command.id)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
<div class="container">
|
||||
@if (seasons()[0]; as serial) {
|
||||
<div class="image">
|
||||
<img
|
||||
[ngSrc]="serial.screenshot_uri"
|
||||
style="position: relative"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<h2>{{ serial.name }}</h2>
|
||||
<div>
|
||||
{{ serial.description }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ serial.director }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
|
||||
{{ serial.actors }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.YEAR' | translate }}:</div>
|
||||
{{ serial.year }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ serial.genres_str }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.AGE' | translate }}:</div>
|
||||
{{ serial.age }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">IMDB Rating:</div>
|
||||
{{ serial.rating_imdb }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Kinopoisk Rating:</div>
|
||||
{{ serial.rating_kinopoisk }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="label">Added:</div>
|
||||
{{ serial.added }}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<app-favorites-button
|
||||
[serialMeta]="{
|
||||
movie_id: serial.id,
|
||||
name: serial.name,
|
||||
cover: serial.screenshot_uri
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="seasons">
|
||||
@for (item of seasons(); track $index) {
|
||||
<mat-divider />
|
||||
<h4>{{ item.name }}</h4>
|
||||
<div class="episodes">
|
||||
@for (episode of item.series; track $index) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="
|
||||
playEpisode.emit({ series: episode, cmd: item.cmd })
|
||||
"
|
||||
>
|
||||
Episode {{ episode }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NgFor, 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 { 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;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-stalker-series-view',
|
||||
templateUrl: './stalker-series-view.component.html',
|
||||
styleUrls: ['../../xtream/detail-view.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
FavoritesButtonComponent,
|
||||
MatButtonModule,
|
||||
MatDividerModule,
|
||||
NgFor,
|
||||
NgOptimizedImage,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
export class StalkerSeriesViewComponent {
|
||||
seasons: Signal<StalkerSeason[]> = input.required({
|
||||
transform: sortByNumericValue,
|
||||
});
|
||||
|
||||
playEpisode = output<{ series: string; cmd: string }>();
|
||||
}
|
||||
@@ -21,6 +21,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.seasons {
|
||||
padding: 20px;
|
||||
|
||||
.episodes {
|
||||
margin-bottom: 10px;
|
||||
|
||||
button {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
width: 300px;
|
||||
padding: 20px 10px;
|
||||
|
||||
Reference in new issue
Block a user