Files
iptvnator/libs/ui/playback/src/lib/fullscreen-episode-panel/fullscreen-episode-panel.component.html
T
4grayandClaude Fable 5.1 4cce4acaad feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups (#1633)
* feat(portal): season thumbnails in the season dropdown + PR #1628 follow-ups

Follow-ups to the season posters shipped in #1628:

- The >6-seasons dropdown (`SeasonTabsComponent`) now carries a 28×42
  season thumbnail at the start of each menu row that has a poster and in
  the closed trigger for the selected season, fed by a new `seasonPosters`
  input from the season container and the fullscreen episode panel. Rows
  without a poster get no placeholder, a failed image is dropped, and the
  pill row stays text-only as the design review decided.
- The fullscreen season strip's episode count uses its own
  `PORTALS.EPISODE_COUNT_ONE/OTHER` keys instead of borrowing the download
  manager's; all 18 locales filled through the i18n merger from their
  existing `DOWNLOADS.EPISODE_COUNT_*` translations.
- The Stalker mock's serve targets no longer pin `PORT` (an nx:run-commands
  `env` entry overrides the shell), and `main.ts` resolves `PORT`, then the
  Playwright-side `MOCK_PORT` alias, then 3210 — so `MOCK_PORT=3310` now
  relocates the whole E2E run. The Xtream mock honours `XTREAM_MOCK_PORT`
  the same way.
- `resolveAutoSelectedSeason` gets a direct spec covering every branch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(xtream-mock): mint marketing asset URLs on the port the server bound

Greptile P1 on #1633: the listener honoured `XTREAM_MOCK_PORT`, but
`marketingAssetOrigin()` still read `PORT` alone, so a run relocated only
through the alias sent every poster/backdrop/logo/episode URL to 3211.

One resolver (`resolveXtreamMockPortString` in `mock-port.ts`: `PORT`,
then `XTREAM_MOCK_PORT`, then 3211) now feeds the environment parser, the
marketing asset origin and the demo-guide origin fallback. A spec pins the
precedence and that `marketingAssetUrl` follows the bound port.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 14:54:54 +02:00

178 lines
8.1 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
? 'PORTALS.EPISODE_COUNT_ONE'
: 'PORTALS.EPISODE_COUNT_OTHER'
) | translate: { count: strip.episodeCount }
}}
</span>
</span>
</div>
}
<app-season-tabs
[seasonKeys]="seasonKeys()"
[selectedSeason]="selectedSeasonKey() ?? undefined"
[episodeCounts]="episodeCounts()"
[watchedCounts]="watchedCounts()"
[seasonPosters]="seasonPosters()"
[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>