From 3f29aa1b0db3becc0cd704faca411f59b05b710a Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 5 Jan 2026 21:56:19 +0100 Subject: [PATCH] style: create vod and serial hero layout --- .../stalker-series-view.component.html | 265 +++++++++--------- .../stalker-series-view.component.ts | 33 ++- .../serial-details.component.html | 135 ++++----- .../serial-details.component.ts | 24 +- .../vod-details/vod-details.component.html | 211 +++++++------- .../vod-details/vod-details.component.ts | 11 +- .../vod-details/vod-details.component.html | 202 ++++++------- .../vod-details/vod-details.component.ts | 26 +- 8 files changed, 447 insertions(+), 460 deletions(-) diff --git a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.html b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.html index d10cfe418..2f5e65d35 100644 --- a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.html +++ b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.html @@ -1,159 +1,154 @@ -@let seasons = seasonsData(); -
- @if (displayItem(); as serial) { -
- @if (serial.info.movie_image) { - Series Cover - } @else { -
- } -
+@let seasons = regularSeasons(); -
-

{{ serial.info.name }}

-
- {{ serial.info.description }} -
- @if (serial.info.director) { -
-
- {{ 'XTREAM.DIRECTOR' | translate }}: -
- {{ serial.info.director }} -
- } - @if (serial.info.actors) { -
-
{{ 'XTREAM.ACTORS' | translate }}:
- {{ serial.info.actors }} -
- } +@if (displayItem(); as serial) { + + + @if (serial.info.releasedate || serial.info.year) { -
-
{{ 'XTREAM.YEAR' | translate }}:
- {{ serial.info.releasedate || serial.info.year }} -
+ {{ + serial.info.releasedate || serial.info.year + }} } @if (serial.info.genre || serial.info.genres_str) { -
-
{{ 'XTREAM.GENRE' | translate }}:
- {{ serial.info.genre || serial.info.genres_str }} -
+ {{ + serial.info.genre || serial.info.genres_str + }} } @if (serial.info.rating_imdb) { -
-
- {{ 'XTREAM.IMDB_RATING' | translate }}: -
- {{ serial.info.rating_imdb }} -
+ + ⭐ {{ serial.info.rating_imdb }} + } @if (serial.info.rating_kinopoisk) { -
-
- {{ 'XTREAM.KINOPOISK_RATING' | translate }}: -
- {{ serial.info.rating_kinopoisk }} + + KP {{ serial.info.rating_kinopoisk }} + + } + + + + + @if (serial.info.actors) { +
+ {{ + 'XTREAM.ACTORS' | translate + }} + {{ serial.info.actors }}
} -
- + @if (serial.info.director) { +
+ {{ + 'XTREAM.DIRECTOR' | translate + }} + {{ serial.info.director }} +
+ } + + + + + + + + + @if (isVodSeries() && isVodSeriesSeasonsLoading()) { +
+ + {{ 'PORTALS.LOADING_SEASONS' | translate }}
-
- } -
+ } - -@if (isVodSeries() && isVodSeriesSeasonsLoading()) { -
- - {{ 'PORTALS.LOADING_SEASONS' | translate }} -
-} - - -@if (!isVodSeries() && isSerialSeasonsLoading()) { -
- - {{ 'PORTALS.LOADING_SEASONS' | translate }} -
-} - -
- @if (isVodSeries()) { - - @for (item of vodSeriesSeasons(); track item.id) { - -
-

{{ item.name }}

- {{ - item.isExpanded ? '▼' : '►' - }} + + @if (!isVodSeries() && isSerialSeasonsLoading()) { +
+ + {{ 'PORTALS.LOADING_SEASONS' | translate }}
+ } - @if (item.isExpanded) { - @if (item.isLoading) { -
- +
+ @if (isVodSeries()) { + + @for (item of vodSeriesSeasons(); track item.id) { + +
+

{{ item.name }}

+ {{ + item.isExpanded ? '▼' : '►' + }} +
+ + @if (item.isExpanded) { + @if (item.isLoading) { +
+ +
+ } @else { +
+ @for ( + episode of item.episodes; + track episode.id ?? $index + ) { + + } + @if ( + !item.episodes || item.episodes.length === 0 + ) { + {{ + 'PORTALS.NO_EPISODES' | translate + }} + } +
+ } + } + } + } @else { + + @for (item of seasons; track item.id ?? $index) { + +
+

{{ item.name }}

- } @else {
- @for ( - episode of item.episodes; - track episode.id || $index - ) { + @for (episode of item.series; track $index) { } - @if (!item.episodes || item.episodes.length === 0) { - {{ - 'PORTALS.NO_EPISODES' | translate - }} - }
} } - } - } @else { - - @for (item of seasons; track item.id || $index) { - -

{{ item.name }}

-
- @for (episode of $any(item).series; track $index) { - - } -
- } - } -
+
+ +} + diff --git a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts index 067dca687..2a9ac29f1 100644 --- a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts +++ b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts @@ -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(() => { 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(); + } } diff --git a/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.html b/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.html index 0ddad12d4..c8ba7f125 100644 --- a/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.html +++ b/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.html @@ -1,99 +1,76 @@ @let item = selectedItem(); @if (item?.info) { -
+ -
- @if (item.info?.cover) { - - } -
-
-

{{ item.info?.name }}

- @if (item.info?.plot) { - - } + + @if (item.info?.releaseDate) { - + {{ + item.info.releaseDate | slice: 0 : 4 + }} } @if (item.info?.genre) { - + {{ item.info.genre }} + } + @if (item.info?.episode_run_time) { + {{ item.info.episode_run_time }} min/ep } @if (item.info?.rating) { - + + ⭐ {{ item.info.rating }} + } + + + + @if (item.info?.cast) { -
-
{{ 'XTREAM.CAST' | translate }}:
- {{ item.info.cast }} +
+ {{ 'XTREAM.CAST' | translate }} + {{ item.info.cast }}
} @if (item.info?.director) { - +
+ {{ + 'XTREAM.DIRECTOR' | translate + }} + {{ item.info.director }} +
} - @if (item.info?.episode_run_time) { - + + + + + @if (!isFavorite()) { + + } @else { + } -
- @if (!isFavorite()) { - - } @else { - - } -
-
+
+ + -
+
} + diff --git a/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.ts b/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.ts index fe6961ef3..f122a21e2 100644 --- a/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.ts +++ b/apps/web/src/app/xtream-tauri/serial-details/serial-details.component.ts @@ -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(); + } } diff --git a/apps/web/src/app/xtream-tauri/vod-details/vod-details.component.html b/apps/web/src/app/xtream-tauri/vod-details/vod-details.component.html index f0ce53f69..5e4d95e5c 100644 --- a/apps/web/src/app/xtream-tauri/vod-details/vod-details.component.html +++ b/apps/web/src/app/xtream-tauri/vod-details/vod-details.component.html @@ -1,123 +1,104 @@ @let item = selectedItem(); @if (item && item.info) { -
+ -
- @if (item.info?.movie_image) { - Movie poster + + + @if (item.info?.releasedate) { + {{ + item.info.releasedate | slice: 0 : 4 + }} + } + @if (item.info?.genre) { + {{ item.info.genre }} + } + @if (item.info?.duration) { + {{ item.info.duration }} + } + @if (item.info?.rating_imdb) { + + ⭐ {{ item.info.rating_imdb }} + + } + @if (item.info?.country) { + {{ item.info.country }} + } + + + + + @if (item.info?.actors || item.info?.cast) { +
+ {{ 'XTREAM.ACTORS' | translate }} + {{ + item.info.actors || item.info.cast + }} +
+ } + @if (item.info?.director) { +
+ {{ + 'XTREAM.DIRECTOR' | translate + }} + {{ item.info.director }} +
+ } + @if (item.info?.rating_kinopoisk) { +
+ {{ + 'XTREAM.KINOPOISK_RATING' | translate + }} + {{ item.info.rating_kinopoisk }} +
+ } +
+ + + + + @if (!isFavorite()) { + } @else { - Default poster + } -
-
-

{{ item?.info?.name || item?.movie_data?.name }}

- @if (item?.info?.description || item?.info?.plot) { -
- {{ item?.info?.description || item?.info?.plot }} + + + + @if (item.info?.youtube_trailer) { +
+

{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}

+
+
- } - @if (item?.info?.releasedate) { -
- {{ 'XTREAM.RELEASE_DATE' | translate }}: -
- {{ item?.info?.releasedate }} - } - @if (item?.info?.genre) { -
{{ 'XTREAM.GENRE' | translate }}:
- {{ item?.info?.genre }} - } - @if (item?.info?.country) { -
{{ 'XTREAM.COUNTRY' | translate }}:
- {{ item?.info?.country }} - } - @if (item?.info?.actors || item?.info?.cast) { -
-
{{ 'XTREAM.ACTORS' | translate }}:
- {{ item?.info?.actors || item?.info?.cast }} -
- } - @if (item?.info?.director) { -
{{ 'XTREAM.DIRECTOR' | translate }}:
- {{ item?.info?.director }} - } - @if (item?.info?.duration) { -
{{ 'XTREAM.DURATION' | translate }}:
- {{ item?.info?.duration }} - } - @if (item?.info?.rating_imdb) { -
{{ 'XTREAM.IMDB_RATING' | translate }}:
- {{ item?.info?.rating_imdb }} - } - @if (item?.info?.rating_kinopoisk) { -
- {{ 'XTREAM.KINOPOISK_RATING' | translate }}: -
- {{ item?.info?.rating_kinopoisk }} - } -
-   - @if (!isFavorite()) { - - } @else { - - }
-
-
- @if (item?.info?.youtube_trailer) { -
-

{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}

- -
- } + } + } + diff --git a/apps/web/src/app/xtream-tauri/vod-details/vod-details.component.ts b/apps/web/src/app/xtream-tauri/vod-details/vod-details.component.ts index 8719fb69a..0e02193bc 100644 --- a/apps/web/src/app/xtream-tauri/vod-details/vod-details.component.ts +++ b/apps/web/src/app/xtream-tauri/vod-details/vod-details.component.ts @@ -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(); + } } diff --git a/apps/web/src/app/xtream/vod-details/vod-details.component.html b/apps/web/src/app/xtream/vod-details/vod-details.component.html index fe2750368..ca9c900be 100644 --- a/apps/web/src/app/xtream/vod-details/vod-details.component.html +++ b/apps/web/src/app/xtream/vod-details/vod-details.component.html @@ -1,115 +1,97 @@ -
+ -
- @if (item.info?.movie_image) { - Movie poster + + + @if (item.info?.releasedate) { + {{ + item.info.releasedate | slice: 0 : 4 + }} + } + @if (item.info?.genre) { + {{ item.info.genre }} + } + @if (item.info?.duration) { + {{ item.info.duration }} + } + @if (item.info?.rating_imdb) { + + ⭐ {{ item.info.rating_imdb }} + + } + @if (item.info?.country) { + {{ item.info.country }} + } + + + + + @if (item.info?.actors) { +
+ {{ 'XTREAM.ACTORS' | translate }} + {{ item.info.actors }} +
+ } + @if (item.info?.director) { +
+ {{ 'XTREAM.DIRECTOR' | translate }} + {{ item.info.director }} +
+ } + @if (item.info?.rating_kinopoisk) { +
+ {{ + 'XTREAM.KINOPOISK_RATING' | translate + }} + {{ item.info.rating_kinopoisk }} +
+ } +
+ + + + + @if (!isFavorite) { + } @else { -
- } -
-
-

{{ item.info.name }}

- @if (item.info.description) { -
- {{ item.info.description }} -
- } - @if (item.info.releasedate) { -
{{ 'XTREAM.RELEASE_DATE' | translate }}:
- {{ item.info.releasedate }} - } - @if (item.info.genre) { -
{{ 'XTREAM.GENRE' | translate }}:
- {{ item.info.genre }} - } - @if (item.info.country) { -
{{ 'XTREAM.COUNTRY' | translate }}:
- {{ item.info.country }} - } - @if (item.info.actors) { -
-
{{ 'XTREAM.ACTORS' | translate }}:
- {{ item.info.actors }} -
- } - @if (item.info.director) { -
{{ 'XTREAM.DIRECTOR' | translate }}:
- {{ item.info.director }} - } - @if (item.info.duration) { -
{{ 'XTREAM.DURATION' | translate }}:
- {{ item.info.duration }} - } - @if (item.info.rating_imdb) { -
{{ 'XTREAM.IMDB_RATING' | translate }}:
- {{ item.info.rating_imdb }} - } - @if (item.info.rating_kinopoisk) { -
- {{ 'XTREAM.KINOPOISK_RATING' | translate }}: -
- {{ item.info.rating_kinopoisk }} - } -
  - @if (!isFavorite) { - - } @else { - - } + check + {{ 'PORTALS.REMOVE_FROM_FAVORITES' | translate }} + + } + + + + @if (item.info?.youtube_trailer) { +
+

{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}

+
+ +
-
-
-@if (item.info.youtube_trailer) { -
-

{{ 'XTREAM.YOUTUBE_TRAILER' | translate }}

- -
-} + } +
+ diff --git a/apps/web/src/app/xtream/vod-details/vod-details.component.ts b/apps/web/src/app/xtream/vod-details/vod-details.component.ts index 21bb0c86e..948406af3 100644 --- a/apps/web/src/app/xtream/vod-details/vod-details.component.ts +++ b/apps/web/src/app/xtream/vod-details/vod-details.component.ts @@ -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(); @Output() removeFromFavoritesClicked = new EventEmitter(); + 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(); + } + } }