mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
* fix(i18n): translate hard-coded labels, snackbars and missing keys Catalog screens, snackbars and external player messages showed English in every locale, and seven keys used in code were missing from en.json, so ngx-translate rendered them raw. - Catalog: "All items", item and channel counts, channel sort menus and tooltips, unnamed-category and empty-category labels, LIVE/PAUSED badges and the Xtream global search summary are translated. The Xtream and Stalker stores no longer bake an English name into the every-item sentinel; the facades return a null title and the view translates it. - Snackbars: Xtream/Stalker request failures, the 413 upload error, backup export/import results and the category visibility failure use keys; every "Close" action uses CLOSE. - External player: the main process sends an error code instead of an English sentence (player-error event, session errorCode, and a tag in rejected launch errors); the renderer, dock and VOD primary button translate it. The external player info dialog is translated. - Adds the seven missing keys and 43 new ones, translated in all 18 locales; seven legitimately identical values are baselined. - tools/i18n/check-usage.mjs fails on keys used in code but missing from en.json; it runs in i18n:check (and so in CI through i18n:validate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): translate catalog counts in the template and skip inline template comments - The category subtitle hands a key to the translate pipe instead of caching translate.instant(), so a cold start re-renders it once the language file loads. - The Xtream live root count uses the singular/plural item keys, so one result no longer reads "1 channels". - check-usage.mjs strips HTML comments inside inline templates of TypeScript files, so a commented-out key no longer fails the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): check keys in parenthesised translate pipe operands `(expanded() ? 'SHOW_LESS' : 'SHOW_MORE') | translate` yields keys that neither precede the pipe directly nor contain a dot, so the usage check missed them. It now reads the ternary and fallback branches of a parenthesised operand; a literal compared in the condition is not read as a key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): expect the translated external player failure in the dock A launch failure without an error code now shows the translated generic status in the playback dock, with the raw main-process detail as its tooltip. The ClearKey DASH flow asserted the raw English text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep the IPC error tag out of the stored session detail A tagged launch failure reached ExternalPlayerSession.error unchanged, so the dock tooltip showed "[iptvnator:external-player:start-failed]". The registry now strips the tag when it stores the detail; the rejected IPC error keeps it for the renderer to read the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(web): expect translated portal request failure toasts #1861's new resolved-failure specs asserted the English toast text; the toasts now go through PORTALS.REQUEST_ERRORS keys, so the specs check the key and its message/status params. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): read only returned branches of a grouped translate operand A literal at the start of a condition, as in `('ERROR' === status() ? 'CLOSE' : 'CLOSE') | translate`, was taken for a translation key, so a valid template could fail the usage check. A grouped literal now counts only when it ends its operand (end of group, `:`, `||` or `??`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
8.0 KiB
HTML
208 lines
8.0 KiB
HTML
<!-- Hero Section with Backdrop -->
|
|
<!-- The skeleton keeps the stage height: whether the title has a backdrop
|
|
is unknown until it loads, and most detail pages do. -->
|
|
<section
|
|
class="hero"
|
|
[class.hero--compact]="layout() === 'compact' && !isLoading()"
|
|
>
|
|
@if (errorMessage()) {
|
|
<div class="hero__content hero__error">
|
|
<mat-icon class="error-icon">cloud_off</mat-icon>
|
|
<h2 class="error-title">{{ errorMessage() }}</h2>
|
|
<ng-content select="[hero-error-actions]"></ng-content>
|
|
</div>
|
|
} @else if (isLoading()) {
|
|
<!-- Placeholders at the loaded hero's geometry, bottom-aligned like
|
|
it: eyebrow, title, chips, two description lines, the action
|
|
row and the credits. -->
|
|
<div
|
|
class="hero__content hero__content--skeleton"
|
|
data-test-id="hero-skeleton"
|
|
aria-hidden="true"
|
|
>
|
|
<div class="poster">
|
|
<span class="skeleton skeleton--poster"></span>
|
|
</div>
|
|
<div class="details">
|
|
<span class="skeleton skeleton--kind"></span>
|
|
<span class="skeleton skeleton--title"></span>
|
|
<span class="details__tags">
|
|
<span class="skeleton skeleton--chip"></span>
|
|
<span class="skeleton skeleton--chip"></span>
|
|
<span class="skeleton skeleton--chip"></span>
|
|
</span>
|
|
<span class="skeleton-lines">
|
|
<span class="skeleton skeleton--line"></span>
|
|
<span class="skeleton skeleton--line"></span>
|
|
</span>
|
|
<span class="action-buttons">
|
|
<span class="skeleton skeleton--primary"></span>
|
|
<span class="skeleton skeleton--icon-button"></span>
|
|
<span class="skeleton skeleton--icon-button"></span>
|
|
</span>
|
|
<span class="skeleton-lines skeleton-lines--meta">
|
|
<span class="skeleton skeleton--line"></span>
|
|
<span class="skeleton skeleton--line"></span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
} @else {
|
|
<!-- Backdrop Image -->
|
|
<div
|
|
class="hero__backdrop"
|
|
[class.hero__backdrop--blurred]="usesPosterBackdrop()"
|
|
[style.backgroundImage]="
|
|
backdropImageUrl() ? null : fallbackBackdropBackground()
|
|
"
|
|
>
|
|
@if (backdropImageUrl(); as backdropImage) {
|
|
<img
|
|
class="hero__backdrop-image"
|
|
[src]="backdropImage"
|
|
(error)="onBackdropError()"
|
|
alt=""
|
|
aria-hidden="true"
|
|
/>
|
|
}
|
|
</div>
|
|
|
|
@if (trailerBackdropUrl(); as trailerUrl) {
|
|
<app-hero-trailer-backdrop
|
|
#trailer
|
|
class="hero__trailer"
|
|
[embedUrl]="trailerUrl"
|
|
[title]="formattedTitle()"
|
|
/>
|
|
<!-- Above the content layer, or the content box would take the clicks -->
|
|
@if (trailer.playing()) {
|
|
<button
|
|
type="button"
|
|
class="hero__trailer-mute"
|
|
[matTooltip]="
|
|
(trailer.muted()
|
|
? 'PORTALS.DETAIL.TRAILER_UNMUTE'
|
|
: 'PORTALS.DETAIL.TRAILER_MUTE'
|
|
) | translate
|
|
"
|
|
[attr.aria-label]="
|
|
(trailer.muted()
|
|
? 'PORTALS.DETAIL.TRAILER_UNMUTE'
|
|
: 'PORTALS.DETAIL.TRAILER_MUTE'
|
|
) | translate
|
|
"
|
|
[attr.aria-pressed]="!trailer.muted()"
|
|
data-test-id="trailer-backdrop-mute"
|
|
(click)="trailer.toggleMute()"
|
|
>
|
|
<mat-icon aria-hidden="true">{{
|
|
trailer.muted() ? 'volume_off' : 'volume_up'
|
|
}}</mat-icon>
|
|
</button>
|
|
}
|
|
}
|
|
|
|
<!-- Scrim: strong on the text side, fading into the page below -->
|
|
<div class="hero__vignette"></div>
|
|
|
|
<!-- Hero Content -->
|
|
<div class="hero__content">
|
|
<!-- Poster Card -->
|
|
<div class="poster">
|
|
@if (posterUrl() && !posterError()) {
|
|
<img
|
|
class="poster__image"
|
|
[src]="posterUrl()"
|
|
(error)="onPosterError()"
|
|
alt=""
|
|
/>
|
|
} @else {
|
|
<div
|
|
class="poster__placeholder"
|
|
[style.backgroundImage]="fallbackPosterBackground()"
|
|
>
|
|
<mat-icon class="poster__placeholder-icon"
|
|
>movie</mat-icon
|
|
>
|
|
<span class="poster__placeholder-text">{{
|
|
formattedTitle()
|
|
}}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div class="details">
|
|
@if (kindLabel(); as kind) {
|
|
<p class="details__kind" data-test-id="detail-kind">
|
|
{{ kind }}
|
|
</p>
|
|
}
|
|
|
|
<h1 class="details__title">{{ formattedTitle() }}</h1>
|
|
|
|
<!-- Tags Slot -->
|
|
<div class="details__tags">
|
|
<ng-content select="[hero-tags]"></ng-content>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
@if (description()) {
|
|
<p
|
|
#descriptionEl
|
|
class="details__description"
|
|
[class.details__description--expanded]="
|
|
isDescriptionExpanded()
|
|
"
|
|
>
|
|
{{ description() }}
|
|
</p>
|
|
@if (hasDescriptionOverflow() || isDescriptionExpanded()) {
|
|
<button
|
|
type="button"
|
|
class="details__description-toggle"
|
|
(click)="toggleDescription()"
|
|
[attr.aria-expanded]="isDescriptionExpanded()"
|
|
>
|
|
<span>{{
|
|
(isDescriptionExpanded()
|
|
? 'SHOW_LESS'
|
|
: 'SHOW_MORE'
|
|
) | translate
|
|
}}</span>
|
|
<mat-icon aria-hidden="true">
|
|
{{
|
|
isDescriptionExpanded()
|
|
? 'expand_less'
|
|
: 'expand_more'
|
|
}}
|
|
</mat-icon>
|
|
</button>
|
|
}
|
|
}
|
|
|
|
@if (progress() !== null) {
|
|
<span
|
|
class="details__progress"
|
|
role="progressbar"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100"
|
|
[attr.aria-valuenow]="progress()"
|
|
data-test-id="detail-progress"
|
|
>
|
|
<i [style.width.%]="progress()"></i>
|
|
</span>
|
|
}
|
|
|
|
<!-- Action Buttons Slot -->
|
|
<div class="action-buttons">
|
|
<ng-content select="[hero-actions]"></ng-content>
|
|
</div>
|
|
|
|
<!-- Credits / metadata slot -->
|
|
<div class="details__meta">
|
|
<ng-content select="[hero-meta]"></ng-content>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</section>
|