mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* 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>
178 lines
8.1 KiB
HTML
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>
|