Files
iptvnator/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.html
T
4grayandClaude Fable 5.1 7790e68147 feat(portal): show each season's own poster beside the season tabs (#1628)
Series detail pages now render the selected season's poster as a season
cover next to the season tabs and description, and the fullscreen episode
panel shows the same poster as a season strip above its tabs.

Resolution is TMDB-first, like the show artwork merge: the lazy season
enrichment stores `/tv/{id}/season/{n}` `poster_path` as a w342 URL in
`tmdb_season_posters` (Xtream) or `StalkerSeriesTmdbSeasonsService.posters()`
(Stalker), under the same write-only-if-changed convergence guard as the
season overview. Xtream falls back to the provider's `seasons[].cover_big`/
`cover` when it is an http(s) URL other than the show poster, because panels
repeat the show poster on every season. Stalker is TMDB-only.

The cover column is not rendered for one-season items, seasons without a
poster, or a failed image, so every fallback is today's markup. It is sized
by a new `--season-cover-width` token (96/120/144px per Settings.coverSize).
The hero poster never follows the season.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 11:45:54 +02:00

177 lines
8.0 KiB
HTML

<div class="episode-panel">
<div class="episode-panel__tabs">
@if (seasonStrip(); as strip) {
<div
class="episode-panel__season"
data-test-id="fullscreen-episode-panel-season"
>
<img
class="episode-panel__season-poster"
[src]="strip.posterUrl"
alt=""
aria-hidden="true"
loading="lazy"
(error)="onSeasonPosterError(strip.posterUrl)"
/>
<span class="episode-panel__season-meta">
<span class="episode-panel__season-name">
{{
'PORTALS.SEASON_TAB'
| translate: { number: strip.seasonKey }
}}
</span>
<span class="episode-panel__season-count">
{{
(strip.episodeCount === 1
? 'DOWNLOADS.EPISODE_COUNT_ONE'
: 'DOWNLOADS.EPISODE_COUNT_OTHER'
) | translate: { count: strip.episodeCount }
}}
</span>
</span>
</div>
}
<app-season-tabs
[seasonKeys]="seasonKeys()"
[selectedSeason]="selectedSeasonKey() ?? undefined"
[episodeCounts]="episodeCounts()"
[watchedCounts]="watchedCounts()"
[playingSeasonKey]="playingSeasonKey()"
(seasonSelected)="onSeasonSelected($event)"
(backToPlayingRequested)="backToPlaying()"
/>
</div>
<ul
#list
class="episode-panel__list"
data-test-id="fullscreen-episode-panel-list"
>
@if (selectedSeason(); as season) {
@if (season.loadState === 'loading') {
<li
class="episode-panel__state"
data-test-id="fullscreen-episode-panel-loading"
>
<mat-spinner diameter="22" />
<span>{{ 'PORTALS.LOADING_EPISODES' | translate }}</span>
</li>
} @else if (season.loadState === 'unloaded') {
<li
class="episode-panel__state episode-panel__state--stacked"
data-test-id="fullscreen-episode-panel-unloaded"
>
<span>{{
'PORTALS.EPISODES_LOAD_FAILED' | translate
}}</span>
<button
type="button"
class="episode-panel__retry"
data-test-id="fullscreen-episode-panel-retry"
(click)="retrySeason(season.key)"
>
<mat-icon aria-hidden="true">refresh</mat-icon>
{{ 'RETRY' | translate }}
</button>
</li>
} @else if (season.episodes.length === 0) {
<li
class="episode-panel__state"
data-test-id="fullscreen-episode-panel-empty"
>
<mat-icon aria-hidden="true">tv_off</mat-icon>
<span>{{ 'PORTALS.SEASON_EMPTY' | translate }}</span>
</li>
} @else {
@for (item of season.episodes; track item.id) {
<li class="episode-panel__row">
<button
type="button"
class="episode-panel__item"
data-test-id="fullscreen-episode-panel-episode"
[class.episode-panel__item--playing]="
item.isPlaying
"
[class.episode-panel__item--watched]="item.watched"
[attr.aria-current]="item.isPlaying ? 'true' : null"
[attr.data-episode-id]="item.id"
(click)="onEpisodeClick(item)"
>
<span
class="episode-panel__thumb"
aria-hidden="true"
>
@if (hasStill(item)) {
<img
class="episode-panel__still"
[src]="item.thumbnailUrl"
alt=""
loading="lazy"
(error)="onStillError(item)"
/>
} @else {
<span class="episode-panel__number">
{{ item.episodeNumber }}
</span>
}
@if (item.isPlaying) {
<span class="episode-panel__playing-badge">
<mat-icon>equalizer</mat-icon>
</span>
}
@if (item.progressPercent !== null) {
<span class="episode-panel__progress">
<span
class="episode-panel__progress-bar"
[style.width.%]="
item.progressPercent
"
></span>
</span>
}
</span>
<span class="episode-panel__meta">
<span class="episode-panel__label">
<span>{{ item.label }}</span>
@if (item.durationLabel) {
<span class="episode-panel__duration">
{{ item.durationLabel }}
</span>
}
@if (item.watched && !item.isPlaying) {
<mat-icon
class="episode-panel__watched"
[attr.aria-label]="
'XTREAM.WATCHED' | translate
"
>check</mat-icon
>
}
@if (item.isPlaying) {
<span
class="episode-panel__now-playing"
>
{{
'PORTALS.NOW_PLAYING'
| translate
}}
</span>
}
</span>
<span class="episode-panel__title">
{{ item.title || item.label }}
</span>
@if (item.overview) {
<span class="episode-panel__overview">
{{ item.overview }}
</span>
}
</span>
</button>
</li>
}
}
}
</ul>
</div>