mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
style: create vod and serial hero layout
This commit is contained in:
1 parent
0662c15905
commit
3f29aa1b0d
8 files changed
+447
-460
No files matched your search
@@ -1,159 +1,154 @@
|
||||
@let seasons = seasonsData();
|
||||
<div class="container">
|
||||
@if (displayItem(); as serial) {
|
||||
<div class="image">
|
||||
@if (serial.info.movie_image) {
|
||||
<img
|
||||
[src]="serial.info.movie_image"
|
||||
style="position: relative"
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
alt="Series Cover"
|
||||
/>
|
||||
} @else {
|
||||
<div class="placeholder-cover"></div>
|
||||
}
|
||||
</div>
|
||||
@let seasons = regularSeasons();
|
||||
|
||||
<div class="details">
|
||||
<h2>{{ serial.info.name }}</h2>
|
||||
<div>
|
||||
{{ serial.info.description }}
|
||||
</div>
|
||||
@if (serial.info.director) {
|
||||
<div>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.DIRECTOR' | translate }}:
|
||||
</div>
|
||||
{{ serial.info.director }}
|
||||
</div>
|
||||
}
|
||||
@if (serial.info.actors) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
|
||||
{{ serial.info.actors }}
|
||||
</div>
|
||||
}
|
||||
@if (displayItem(); as serial) {
|
||||
<app-content-hero
|
||||
[title]="serial.info.name"
|
||||
[description]="serial.info.description"
|
||||
[posterUrl]="serial.info.movie_image"
|
||||
(backClicked)="goBack()"
|
||||
>
|
||||
<!-- Tags: Year, Genre, Rating -->
|
||||
<ng-container hero-tags>
|
||||
@if (serial.info.releasedate || serial.info.year) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.YEAR' | translate }}:</div>
|
||||
{{ serial.info.releasedate || serial.info.year }}
|
||||
</div>
|
||||
<span class="details__tag">{{
|
||||
serial.info.releasedate || serial.info.year
|
||||
}}</span>
|
||||
}
|
||||
@if (serial.info.genre || serial.info.genres_str) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ serial.info.genre || serial.info.genres_str }}
|
||||
</div>
|
||||
<span class="details__tag">{{
|
||||
serial.info.genre || serial.info.genres_str
|
||||
}}</span>
|
||||
}
|
||||
@if (serial.info.rating_imdb) {
|
||||
<div>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.IMDB_RATING' | translate }}:
|
||||
</div>
|
||||
{{ serial.info.rating_imdb }}
|
||||
</div>
|
||||
<span class="details__tag details__tag--rating">
|
||||
⭐ {{ serial.info.rating_imdb }}
|
||||
</span>
|
||||
}
|
||||
@if (serial.info.rating_kinopoisk) {
|
||||
<div>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
|
||||
</div>
|
||||
{{ serial.info.rating_kinopoisk }}
|
||||
<span class="details__tag">
|
||||
KP {{ serial.info.rating_kinopoisk }}
|
||||
</span>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<!-- Metadata Grid -->
|
||||
<ng-container hero-meta>
|
||||
@if (serial.info.actors) {
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{
|
||||
'XTREAM.ACTORS' | translate
|
||||
}}</span>
|
||||
<span class="value">{{ serial.info.actors }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="action-buttons">
|
||||
<app-favorites-button
|
||||
[itemId]="serial.id"
|
||||
(addToFavoritesClicked)="addToFavorites(serial)"
|
||||
(removeFromFavoritesClicked)="
|
||||
removeFromFavorites(serial.id)
|
||||
"
|
||||
/>
|
||||
@if (serial.info.director) {
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{
|
||||
'XTREAM.DIRECTOR' | translate
|
||||
}}</span>
|
||||
<span class="value">{{ serial.info.director }}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<ng-container hero-actions>
|
||||
<app-favorites-button
|
||||
[itemId]="serial.id"
|
||||
(addToFavoritesClicked)="addToFavorites(serial)"
|
||||
(removeFromFavoritesClicked)="removeFromFavorites(serial.id)"
|
||||
/>
|
||||
</ng-container>
|
||||
|
||||
<!-- Loading state for VOD series seasons -->
|
||||
@if (isVodSeries() && isVodSeriesSeasonsLoading()) {
|
||||
<div class="seasons-loading">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<span>{{ 'PORTALS.LOADING_SEASONS' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Loading state for VOD series seasons -->
|
||||
@if (isVodSeries() && isVodSeriesSeasonsLoading()) {
|
||||
<div class="seasons-loading">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<span>{{ 'PORTALS.LOADING_SEASONS' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Loading state for regular series seasons -->
|
||||
@if (!isVodSeries() && isSerialSeasonsLoading()) {
|
||||
<div class="seasons-loading">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<span>{{ 'PORTALS.LOADING_SEASONS' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="seasons">
|
||||
@if (isVodSeries()) {
|
||||
<!-- VOD series mode: expandable seasons with dynamic episode loading -->
|
||||
@for (item of vodSeriesSeasons(); track item.id) {
|
||||
<mat-divider />
|
||||
<div class="season-header" (click)="toggleSeasonExpanded(item)">
|
||||
<h4>{{ item.name }}</h4>
|
||||
<span class="expand-icon">{{
|
||||
item.isExpanded ? '▼' : '►'
|
||||
}}</span>
|
||||
<!-- Loading state for regular series seasons -->
|
||||
@if (!isVodSeries() && isSerialSeasonsLoading()) {
|
||||
<div class="seasons-loading">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<span>{{ 'PORTALS.LOADING_SEASONS' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (item.isExpanded) {
|
||||
@if (item.isLoading) {
|
||||
<div class="episodes-loading">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
<div class="seasons">
|
||||
@if (isVodSeries()) {
|
||||
<!-- VOD series mode: expandable seasons with dynamic episode loading -->
|
||||
@for (item of vodSeriesSeasons(); track item.id) {
|
||||
<mat-divider />
|
||||
<div
|
||||
class="season-header"
|
||||
(click)="toggleSeasonExpanded(item)"
|
||||
>
|
||||
<h4>{{ item.name }}</h4>
|
||||
<span class="expand-icon">{{
|
||||
item.isExpanded ? '▼' : '►'
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
@if (item.isExpanded) {
|
||||
@if (item.isLoading) {
|
||||
<div class="episodes-loading">
|
||||
<mat-spinner diameter="24"></mat-spinner>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="episodes">
|
||||
@for (
|
||||
episode of item.episodes;
|
||||
track episode.id ?? $index
|
||||
) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="
|
||||
playVodSeriesEpisode(episode);
|
||||
$event.stopPropagation()
|
||||
"
|
||||
>
|
||||
{{
|
||||
episode.name ||
|
||||
('XTREAM.EPISODE' | translate) +
|
||||
' ' +
|
||||
episode.series_number
|
||||
}}
|
||||
</button>
|
||||
}
|
||||
@if (
|
||||
!item.episodes || item.episodes.length === 0
|
||||
) {
|
||||
<span class="no-episodes">{{
|
||||
'PORTALS.NO_EPISODES' | translate
|
||||
}}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<!-- Regular series mode: pre-loaded episodes -->
|
||||
@for (item of seasons; track item.id ?? $index) {
|
||||
<mat-divider />
|
||||
<div class="season-header">
|
||||
<h4>{{ item.name }}</h4>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="episodes">
|
||||
@for (
|
||||
episode of item.episodes;
|
||||
track episode.id || $index
|
||||
) {
|
||||
@for (episode of item.series; track $index) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="
|
||||
playVodSeriesEpisode(episode);
|
||||
$event.stopPropagation()
|
||||
"
|
||||
(click)="playEpisodeClicked(episode, item.cmd)"
|
||||
>
|
||||
{{
|
||||
episode.name ||
|
||||
('XTREAM.EPISODE' | translate) +
|
||||
' ' +
|
||||
episode.series_number
|
||||
}}
|
||||
{{ 'XTREAM.EPISODE' | translate }}
|
||||
{{ episode }}
|
||||
</button>
|
||||
}
|
||||
@if (!item.episodes || item.episodes.length === 0) {
|
||||
<span class="no-episodes">{{
|
||||
'PORTALS.NO_EPISODES' | translate
|
||||
}}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<!-- Regular series mode: pre-loaded episodes -->
|
||||
@for (item of seasons; track item.id || $index) {
|
||||
<mat-divider />
|
||||
<h4>{{ item.name }}</h4>
|
||||
<div class="episodes">
|
||||
@for (episode of $any(item).series; track $index) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
(click)="playEpisodeClicked(episode, $any(item).cmd)"
|
||||
>
|
||||
{{ 'XTREAM.EPISODE' | translate }} {{ episode }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</app-content-hero>
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { MatDivider } from '@angular/material/divider';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { FavoritesButtonComponent } from '../favorites-button/favorites-button.component';
|
||||
import { ContentHeroComponent } from 'components';
|
||||
import { StalkerStore } from '../stalker.store';
|
||||
|
||||
/**
|
||||
@@ -26,6 +27,16 @@ interface VodSeriesSeason {
|
||||
isExpanded?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common interface for regular series seasons
|
||||
*/
|
||||
interface StalkerSeriesSeason {
|
||||
id: string;
|
||||
name: string;
|
||||
cmd?: string;
|
||||
series?: any[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying series/episodes for Stalker portal content.
|
||||
* Supports three modes:
|
||||
@@ -36,9 +47,10 @@ interface VodSeriesSeason {
|
||||
@Component({
|
||||
selector: 'app-stalker-series-view',
|
||||
templateUrl: './stalker-series-view.component.html',
|
||||
styleUrls: ['../../xtream/detail-view.scss'],
|
||||
styleUrls: ['../../xtream-tauri/detail-view.scss'],
|
||||
imports: [
|
||||
FavoritesButtonComponent,
|
||||
ContentHeroComponent,
|
||||
MatButton,
|
||||
MatDivider,
|
||||
MatProgressSpinner,
|
||||
@@ -112,9 +124,8 @@ export class StalkerSeriesViewComponent {
|
||||
/**
|
||||
* For VOD with embedded series, we create a single "season" with the episodes
|
||||
* For regular series, we use the API-fetched seasons
|
||||
* For VOD series (is_series=1), we use the fetched seasons with dynamic episode loading
|
||||
*/
|
||||
readonly seasonsData = computed(() => {
|
||||
readonly regularSeasons = computed<StalkerSeriesSeason[]>(() => {
|
||||
const vodItem = this.vodWithSeries();
|
||||
if (vodItem?.series?.length > 0) {
|
||||
// VOD with embedded series - create a pseudo-season structure
|
||||
@@ -128,15 +139,8 @@ export class StalkerSeriesViewComponent {
|
||||
];
|
||||
}
|
||||
|
||||
// Check if this is a VOD series (is_series=1)
|
||||
const displayItem = this.displayItem();
|
||||
if (displayItem?.is_series === true) {
|
||||
// Return VOD series seasons (episodes loaded dynamically)
|
||||
return this.vodSeriesSeasons();
|
||||
}
|
||||
|
||||
// Regular series - use API-fetched seasons
|
||||
return this.stalkerStore.getSerialSeasonsResource();
|
||||
return this.stalkerStore.getSerialSeasonsResource() as unknown as StalkerSeriesSeason[];
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -213,7 +217,7 @@ export class StalkerSeriesViewComponent {
|
||||
/**
|
||||
* Play episode - handles both regular series and VOD series
|
||||
*/
|
||||
playEpisodeClicked(episode: any, cmd: string) {
|
||||
playEpisodeClicked(episode: any, cmd?: string) {
|
||||
const item = this.displayItem();
|
||||
this.stalkerStore.createLinkToPlayVod(
|
||||
cmd,
|
||||
@@ -255,4 +259,9 @@ export class StalkerSeriesViewComponent {
|
||||
removeFromFavorites(serialId: string) {
|
||||
this.stalkerStore.removeFromFavorites(serialId);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
// Clear selectedItem to return to category view
|
||||
this.stalkerStore.clearSelectedItem();
|
||||
}
|
||||
}
|
||||
@@ -1,99 +1,76 @@
|
||||
@let item = selectedItem();
|
||||
@if (item?.info) {
|
||||
<div
|
||||
class="container"
|
||||
[style.background]="
|
||||
item.info?.backdrop_path && item.info.backdrop_path.length > 0
|
||||
? 'linear-gradient(to top, rgba(29,29,29,1) 0%, rgba(0,0,0,0.5) 100%), url(' +
|
||||
item.info.backdrop_path[0] +
|
||||
') repeat-x'
|
||||
: 'none'
|
||||
"
|
||||
<!-- Hero Section with Backdrop -->
|
||||
<app-content-hero
|
||||
[title]="item.info?.name"
|
||||
[description]="item.info?.plot"
|
||||
[posterUrl]="item.info?.cover"
|
||||
[backdropUrl]="item.info?.backdrop_path?.[0]"
|
||||
(backClicked)="goBack()"
|
||||
>
|
||||
<div class="image">
|
||||
@if (item.info?.cover) {
|
||||
<img
|
||||
[src]="item.info.cover"
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item.info?.name }}</h2>
|
||||
@if (item.info?.plot) {
|
||||
<label>
|
||||
{{ item.info.plot }}
|
||||
</label>
|
||||
}
|
||||
<!-- Tags: Year, Genre, Episode Runtime, Rating -->
|
||||
<ng-container hero-tags>
|
||||
@if (item.info?.releaseDate) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.RELEASE_DATE' | translate }}:
|
||||
</div>
|
||||
{{ item.info.releaseDate }}
|
||||
</label>
|
||||
<span class="details__tag">{{
|
||||
item.info.releaseDate | slice: 0 : 4
|
||||
}}</span>
|
||||
}
|
||||
@if (item.info?.genre) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item.info.genre }}
|
||||
</label>
|
||||
<span class="details__tag">{{ item.info.genre }}</span>
|
||||
}
|
||||
@if (item.info?.episode_run_time) {
|
||||
<span class="details__tag"
|
||||
>{{ item.info.episode_run_time }} min/ep</span
|
||||
>
|
||||
}
|
||||
@if (item.info?.rating) {
|
||||
<label>
|
||||
<div class="label">{{ 'XTREAM.RATING' | translate }}:</div>
|
||||
{{ item.info?.rating }}
|
||||
</label>
|
||||
<span class="details__tag details__tag--rating">
|
||||
⭐ {{ item.info.rating }}
|
||||
</span>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<!-- Metadata Grid -->
|
||||
<ng-container hero-meta>
|
||||
@if (item.info?.cast) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.CAST' | translate }}:</div>
|
||||
{{ item.info.cast }}
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{ 'XTREAM.CAST' | translate }}</span>
|
||||
<span class="value">{{ item.info.cast }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (item.info?.director) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.DIRECTOR' | translate }}:
|
||||
</div>
|
||||
{{ item.info.director }}
|
||||
</label>
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{
|
||||
'XTREAM.DIRECTOR' | translate
|
||||
}}</span>
|
||||
<span class="value">{{ item.info.director }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (item.info?.episode_run_time) {
|
||||
<label>
|
||||
<div class="label">
|
||||
{{ 'XTREAM.EPISODE_RUN_TIME' | translate }}:
|
||||
</div>
|
||||
{{ item.info.episode_run_time }}
|
||||
</label>
|
||||
</ng-container>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<ng-container hero-actions>
|
||||
@if (!isFavorite()) {
|
||||
<button class="favorite-btn" (click)="toggleFavorite()">
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
class="favorite-btn favorite-btn--active"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>check</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
<div class="action-buttons">
|
||||
@if (!isFavorite()) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<!-- Seasons & Episodes -->
|
||||
<app-season-container
|
||||
[seasons]="item.episodes"
|
||||
(episodeClicked)="playEpisode($event)"
|
||||
/>
|
||||
</div>
|
||||
</app-content-hero>
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { Location, SlicePipe } from '@angular/common';
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ContentHeroComponent } from 'components';
|
||||
import { XtreamSerieEpisode } from 'shared-interfaces';
|
||||
import { SeasonContainerComponent } from '../season-container/season-container.component';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
@@ -11,9 +12,16 @@ import { XtreamStore } from '../xtream.store';
|
||||
selector: 'app-serial-details',
|
||||
templateUrl: './serial-details.component.html',
|
||||
styleUrls: ['../detail-view.scss'],
|
||||
imports: [MatButton, MatIcon, SeasonContainerComponent, TranslatePipe],
|
||||
imports: [
|
||||
ContentHeroComponent,
|
||||
MatIcon,
|
||||
SeasonContainerComponent,
|
||||
SlicePipe,
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
export class SerialDetailsComponent {
|
||||
export class SerialDetailsComponent implements OnInit, OnDestroy {
|
||||
private readonly location = inject(Location);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
|
||||
@@ -33,6 +41,10 @@ export class SerialDetailsComponent {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.xtreamStore.setSelectedItem(null);
|
||||
}
|
||||
|
||||
playEpisode(episode: XtreamSerieEpisode) {
|
||||
this.addToRecentlyViewed(this.route.snapshot.params.serialId);
|
||||
|
||||
@@ -57,4 +69,8 @@ export class SerialDetailsComponent {
|
||||
this.xtreamStore.currentPlaylist().id
|
||||
);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
@@ -1,123 +1,104 @@
|
||||
@let item = selectedItem();
|
||||
@if (item && item.info) {
|
||||
<div
|
||||
class="container"
|
||||
[style.background]="
|
||||
item?.info?.backdrop_path && item?.info?.backdrop_path.length > 0
|
||||
? theme() === 'DARK_THEME'
|
||||
? 'linear-gradient(to top, rgba(29,29,29,1) 0%, rgba(0,0,0,0.5) 100%), url(' +
|
||||
item?.info?.backdrop_path[0] +
|
||||
') repeat-x'
|
||||
: 'linear-gradient(to top, rgba(255,255,255,1) 0%, rgba(255,255,255,0.5) 100%), url(' +
|
||||
item?.info?.backdrop_path[0] +
|
||||
') repeat-x'
|
||||
: 'none'
|
||||
"
|
||||
<!-- Hero Section with Backdrop -->
|
||||
<app-content-hero
|
||||
[title]="item.info?.name || item?.movie_data?.name"
|
||||
[description]="item.info.description || item.info.plot"
|
||||
[posterUrl]="item.info.movie_image"
|
||||
[backdropUrl]="item.info?.backdrop_path?.[0]"
|
||||
(backClicked)="goBack()"
|
||||
>
|
||||
<div class="image">
|
||||
@if (item.info?.movie_image) {
|
||||
<img
|
||||
[src]="item.info?.movie_image"
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
alt="Movie poster"
|
||||
/>
|
||||
<!-- Tags: Year, Genre, Duration, Rating -->
|
||||
<ng-container hero-tags>
|
||||
@if (item.info?.releasedate) {
|
||||
<span class="details__tag">{{
|
||||
item.info.releasedate | slice: 0 : 4
|
||||
}}</span>
|
||||
}
|
||||
@if (item.info?.genre) {
|
||||
<span class="details__tag">{{ item.info.genre }}</span>
|
||||
}
|
||||
@if (item.info?.duration) {
|
||||
<span class="details__tag">{{ item.info.duration }}</span>
|
||||
}
|
||||
@if (item.info?.rating_imdb) {
|
||||
<span class="details__tag details__tag--rating">
|
||||
⭐ {{ item.info.rating_imdb }}
|
||||
</span>
|
||||
}
|
||||
@if (item.info?.country) {
|
||||
<span class="details__tag">{{ item.info.country }}</span>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<!-- Metadata Grid -->
|
||||
<ng-container hero-meta>
|
||||
@if (item.info?.actors || item.info?.cast) {
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{ 'XTREAM.ACTORS' | translate }}</span>
|
||||
<span class="value">{{
|
||||
item.info.actors || item.info.cast
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (item.info?.director) {
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{
|
||||
'XTREAM.DIRECTOR' | translate
|
||||
}}</span>
|
||||
<span class="value">{{ item.info.director }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (item.info?.rating_kinopoisk) {
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{
|
||||
'XTREAM.KINOPOISK_RATING' | translate
|
||||
}}</span>
|
||||
<span class="value">{{ item.info.rating_kinopoisk }}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<ng-container hero-actions>
|
||||
<button class="play-btn" (click)="playVod(item)">
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{ 'XTREAM.PLAY' | translate }}
|
||||
</button>
|
||||
@if (!isFavorite()) {
|
||||
<button class="favorite-btn" (click)="toggleFavorite()">
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<img
|
||||
src="./assets/images/default-poster.png"
|
||||
alt="Default poster"
|
||||
/>
|
||||
<button
|
||||
class="favorite-btn favorite-btn--active"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>check</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item?.info?.name || item?.movie_data?.name }}</h2>
|
||||
@if (item?.info?.description || item?.info?.plot) {
|
||||
<div>
|
||||
{{ item?.info?.description || item?.info?.plot }}
|
||||
</ng-container>
|
||||
|
||||
<!-- YouTube Trailer -->
|
||||
@if (item.info?.youtube_trailer) {
|
||||
<div class="youtube-trailer">
|
||||
<h3>{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}</h3>
|
||||
<div class="trailer-wrapper">
|
||||
<iframe
|
||||
[src]="
|
||||
'https://www.youtube.com/embed/' +
|
||||
item.info.youtube_trailer | safe
|
||||
"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
}
|
||||
@if (item?.info?.releasedate) {
|
||||
<div class="label">
|
||||
{{ 'XTREAM.RELEASE_DATE' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.releasedate }}
|
||||
}
|
||||
@if (item?.info?.genre) {
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item?.info?.genre }}
|
||||
}
|
||||
@if (item?.info?.country) {
|
||||
<div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
|
||||
{{ item?.info?.country }}
|
||||
}
|
||||
@if (item?.info?.actors || item?.info?.cast) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
|
||||
{{ item?.info?.actors || item?.info?.cast }}
|
||||
</div>
|
||||
}
|
||||
@if (item?.info?.director) {
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ item?.info?.director }}
|
||||
}
|
||||
@if (item?.info?.duration) {
|
||||
<div class="label">{{ 'XTREAM.DURATION' | translate }}:</div>
|
||||
{{ item?.info?.duration }}
|
||||
}
|
||||
@if (item?.info?.rating_imdb) {
|
||||
<div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div>
|
||||
{{ item?.info?.rating_imdb }}
|
||||
}
|
||||
@if (item?.info?.rating_kinopoisk) {
|
||||
<div class="label">
|
||||
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
|
||||
</div>
|
||||
{{ item?.info?.rating_kinopoisk }}
|
||||
}
|
||||
<div class="action-buttons">
|
||||
<button mat-flat-button color="accent" (click)="playVod(item)">
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{ 'XTREAM.PLAY' | translate }}</button
|
||||
>
|
||||
@if (!isFavorite()) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (item?.info?.youtube_trailer) {
|
||||
<div class="youtube-trailer">
|
||||
<h3>{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}</h3>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="450"
|
||||
[src]="
|
||||
'https://www.youtube.com/embed/' +
|
||||
item.info?.youtube_trailer | safe
|
||||
"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</app-content-hero>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Location, SlicePipe } from '@angular/common';
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ContentHeroComponent } from 'components';
|
||||
import { XtreamVodDetails } from 'shared-interfaces';
|
||||
import { SettingsStore } from '../../services/settings-store.service';
|
||||
import { XtreamStore } from '../xtream.store';
|
||||
@@ -13,14 +14,16 @@ import { SafePipe } from './safe.pipe';
|
||||
templateUrl: './vod-details.component.html',
|
||||
styleUrls: ['../detail-view.scss'],
|
||||
imports: [
|
||||
MatButton,
|
||||
ContentHeroComponent,
|
||||
MatIcon,
|
||||
SafePipe,
|
||||
SlicePipe,
|
||||
TranslateModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
})
|
||||
export class VodDetailsComponent implements OnInit, OnDestroy {
|
||||
private location = inject(Location);
|
||||
private settingsStore = inject(SettingsStore);
|
||||
private route = inject(ActivatedRoute);
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
@@ -67,4 +70,8 @@ export class VodDetailsComponent implements OnInit, OnDestroy {
|
||||
playlist: this.xtreamStore.currentPlaylist,
|
||||
});
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
@@ -1,115 +1,97 @@
|
||||
<div
|
||||
class="container"
|
||||
[style.background]="
|
||||
item?.info?.backdrop_path && item?.info?.backdrop_path.length > 0
|
||||
? 'linear-gradient(to top, rgba(29,29,29,1) 0%, rgba(0,0,0,0.5) 100%), url(' +
|
||||
item?.info?.backdrop_path[0] +
|
||||
') repeat-x'
|
||||
: 'none'
|
||||
"
|
||||
<!-- Hero Section with Backdrop -->
|
||||
<app-content-hero
|
||||
[title]="item.info.name"
|
||||
[description]="item.info.description"
|
||||
[posterUrl]="item.info.movie_image"
|
||||
[backdropUrl]="item.info.backdrop_path?.[0]"
|
||||
(backClicked)="goBack()"
|
||||
>
|
||||
<div class="image">
|
||||
@if (item.info?.movie_image) {
|
||||
<img
|
||||
[src]="item.info?.movie_image"
|
||||
(error)="
|
||||
$event.target.src = './assets/images/default-poster.png'
|
||||
"
|
||||
alt="Movie poster"
|
||||
/>
|
||||
<!-- Tags: Year, Genre, Duration, Rating -->
|
||||
<ng-container hero-tags>
|
||||
@if (item.info?.releasedate) {
|
||||
<span class="details__tag">{{
|
||||
item.info.releasedate | slice: 0 : 4
|
||||
}}</span>
|
||||
}
|
||||
@if (item.info?.genre) {
|
||||
<span class="details__tag">{{ item.info.genre }}</span>
|
||||
}
|
||||
@if (item.info?.duration) {
|
||||
<span class="details__tag">{{ item.info.duration }}</span>
|
||||
}
|
||||
@if (item.info?.rating_imdb) {
|
||||
<span class="details__tag details__tag--rating">
|
||||
⭐ {{ item.info.rating_imdb }}
|
||||
</span>
|
||||
}
|
||||
@if (item.info?.country) {
|
||||
<span class="details__tag">{{ item.info.country }}</span>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<!-- Metadata Grid -->
|
||||
<ng-container hero-meta>
|
||||
@if (item.info?.actors) {
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{ 'XTREAM.ACTORS' | translate }}</span>
|
||||
<span class="value">{{ item.info.actors }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (item.info?.director) {
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{ 'XTREAM.DIRECTOR' | translate }}</span>
|
||||
<span class="value">{{ item.info.director }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (item.info?.rating_kinopoisk) {
|
||||
<div class="details__meta-item">
|
||||
<span class="label">{{
|
||||
'XTREAM.KINOPOISK_RATING' | translate
|
||||
}}</span>
|
||||
<span class="value">{{ item.info.rating_kinopoisk }}</span>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<ng-container hero-actions>
|
||||
<button class="play-btn" (click)="playClicked.emit(item)">
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{ 'XTREAM.PLAY' | translate }}
|
||||
</button>
|
||||
@if (!isFavorite) {
|
||||
<button class="favorite-btn" (click)="toggleFavorite()">
|
||||
<mat-icon>add</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<div class="placeholder-cover"></div>
|
||||
}
|
||||
</div>
|
||||
<div class="details">
|
||||
<h2>{{ item.info.name }}</h2>
|
||||
@if (item.info.description) {
|
||||
<div>
|
||||
{{ item.info.description }}
|
||||
</div>
|
||||
}
|
||||
@if (item.info.releasedate) {
|
||||
<div class="label">{{ 'XTREAM.RELEASE_DATE' | translate }}:</div>
|
||||
{{ item.info.releasedate }}
|
||||
}
|
||||
@if (item.info.genre) {
|
||||
<div class="label">{{ 'XTREAM.GENRE' | translate }}:</div>
|
||||
{{ item.info.genre }}
|
||||
}
|
||||
@if (item.info.country) {
|
||||
<div class="label">{{ 'XTREAM.COUNTRY' | translate }}:</div>
|
||||
{{ item.info.country }}
|
||||
}
|
||||
@if (item.info.actors) {
|
||||
<div>
|
||||
<div class="label">{{ 'XTREAM.ACTORS' | translate }}:</div>
|
||||
{{ item.info.actors }}
|
||||
</div>
|
||||
}
|
||||
@if (item.info.director) {
|
||||
<div class="label">{{ 'XTREAM.DIRECTOR' | translate }}:</div>
|
||||
{{ item.info.director }}
|
||||
}
|
||||
@if (item.info.duration) {
|
||||
<div class="label">{{ 'XTREAM.DURATION' | translate }}:</div>
|
||||
{{ item.info.duration }}
|
||||
}
|
||||
@if (item.info.rating_imdb) {
|
||||
<div class="label">{{ 'XTREAM.IMDB_RATING' | translate }}:</div>
|
||||
{{ item.info.rating_imdb }}
|
||||
}
|
||||
@if (item.info.rating_kinopoisk) {
|
||||
<div class="label">
|
||||
{{ 'XTREAM.KINOPOISK_RATING' | translate }}:
|
||||
</div>
|
||||
{{ item.info.rating_kinopoisk }}
|
||||
}
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="accent"
|
||||
(click)="playClicked.emit(item)"
|
||||
class="favorite-btn favorite-btn--active"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>play_arrow</mat-icon>
|
||||
{{ 'XTREAM.PLAY' | translate }}</button
|
||||
>
|
||||
@if (!isFavorite) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star_outline</mat-icon>
|
||||
{{ 'PORTALS.ADD_TO_FAVORITES' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="accent"
|
||||
(click)="toggleFavorite()"
|
||||
>
|
||||
<mat-icon>star</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
<mat-icon>check</mat-icon>
|
||||
{{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }}
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
|
||||
<!-- YouTube Trailer -->
|
||||
@if (item.info?.youtube_trailer) {
|
||||
<div class="youtube-trailer">
|
||||
<h3>{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}</h3>
|
||||
<div class="trailer-wrapper">
|
||||
<iframe
|
||||
[src]="
|
||||
'https://www.youtube.com/embed/' +
|
||||
item.info.youtube_trailer | safe
|
||||
"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (item.info.youtube_trailer) {
|
||||
<div class="youtube-trailer">
|
||||
<h3>{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}</h3>
|
||||
<iframe
|
||||
width="100%"
|
||||
height="450"
|
||||
[src]="
|
||||
'https://www.youtube.com/embed/' + item.info?.youtube_trailer
|
||||
| safe
|
||||
"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</app-content-hero>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Location, SlicePipe } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
@@ -6,24 +7,26 @@ import {
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ContentHeroComponent } from 'components';
|
||||
import { PlaylistsService } from 'services';
|
||||
import { XtreamVodDetails } from 'shared-interfaces';
|
||||
import { StalkerStore } from '../../stalker/stalker.store';
|
||||
import { SafePipe } from '../../xtream-tauri/vod-details/safe.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'app-vod-details',
|
||||
templateUrl: './vod-details.component.html',
|
||||
styleUrls: ['../detail-view.scss'],
|
||||
styleUrls: ['../../xtream-tauri/detail-view.scss'],
|
||||
imports: [
|
||||
MatButtonModule,
|
||||
ContentHeroComponent,
|
||||
MatIcon,
|
||||
MatProgressSpinnerModule,
|
||||
SafePipe,
|
||||
SlicePipe,
|
||||
TranslatePipe,
|
||||
],
|
||||
})
|
||||
@@ -34,12 +37,19 @@ export class VodDetailsComponent implements OnInit {
|
||||
@Output() playClicked = new EventEmitter<XtreamVodDetails>();
|
||||
@Output() removeFromFavoritesClicked = new EventEmitter<number>();
|
||||
|
||||
private location = inject(Location);
|
||||
private playlistService = inject(PlaylistsService);
|
||||
private route = inject(ActivatedRoute);
|
||||
private stalkerStore = inject(StalkerStore);
|
||||
private portalId =
|
||||
this.route.snapshot.paramMap.get('id') ??
|
||||
this.route.parent.snapshot.params.id;
|
||||
|
||||
/** Check if this is a Stalker item (has cmd property) */
|
||||
private get isStalker(): boolean {
|
||||
return !!(this.item as any)?.cmd;
|
||||
}
|
||||
|
||||
isFavorite = false;
|
||||
isLoading = false;
|
||||
|
||||
@@ -102,4 +112,14 @@ export class VodDetailsComponent implements OnInit {
|
||||
}
|
||||
this.isFavorite = !this.isFavorite;
|
||||
}
|
||||
|
||||
goBack() {
|
||||
if (this.isStalker) {
|
||||
// Stalker mode: clear selectedItem to return to category view
|
||||
this.stalkerStore.clearSelectedItem();
|
||||
} else {
|
||||
// Xtream mode: use browser history navigation
|
||||
this.location.back();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user