mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
* feat(portals): posters-only cover wall for movie and series grids Add `Settings.showCoverTitles` (Settings > General, default on). Turning it off drops the title row under VOD/series covers in catalog, favorites and recent grids and reveals the title as a bottom-gradient overlay on hover and keyboard focus, pinned open for items whose cover is missing or failed. `CoverTitlesService` is the single resolver: the opt-out AND a hover-capable pointer, so touch-only devices keep their titles. Live channel grids, search results, "recently added" rails and dashboard rails always keep labels. Catalog and collection cards become keyboard buttons (role, tabindex, aria-label, Enter/Space, focus ring) and poster alt text is the title. The default-on boolean coercion moves into `settings-opt-out.util.ts` because the settings store reached the max-lines limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep nested Remove key presses from activating the card Enter/Space on the content card's nested Remove button bubbled into the card's own key handlers: Enter opened the item before removing it and Space opened it while cancelling the removal. Only keys pressed on the card element itself now activate it. Regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep cover titles while an in-section search filters the grid The posters-only wall exempts search results because they are identified by the name the user typed; the category grid's own in-section filter is the same case, so `app-grid-list` now keeps the title row while its `searchTerm` is non-blank. Contract docs updated, regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): keep cover titles while the collection tab search is active The unified favorites/recent tab filters by its own search term, so its matches are identified by name like every other search result. The tab now opts its cards out of the posters-only wall while the term is non-blank. Contract docs updated, regression spec added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): move the card Remove control out of the button surface An interactive control nested inside a role="button" is an invalid accessibility structure. The content card's activation surface is now its own inner element and the Remove button a sibling positioned over the poster corner, labelled by its tooltip text. Spec asserts the control is never a descendant of the button. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): draw the collection card focus ring where it is not clipped The card's overflow: hidden clipped an outline drawn on the inner activation surface on every edge, so keyboard users saw no focus indication. The ring now sits on the outer card via :has(> .content-card__activation:focus-visible). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(portals): detect any hover-capable pointer for the posters-only wall `hover` describes only the primary pointer, so a touch-first tablet with a mouse or hover-capable stylus attached lost the wall. The resolver now reads `(any-hover: hover)`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
267 lines
10 KiB
HTML
267 lines
10 KiB
HTML
<section class="settings-group" id="general" [formGroup]="form()">
|
|
<header class="settings-group__header">
|
|
<div class="settings-group__header-icon">
|
|
<mat-icon>settings</mat-icon>
|
|
</div>
|
|
<div class="settings-group__header-text">
|
|
<h3>{{ 'SETTINGS.GENERAL' | translate }}</h3>
|
|
<p>{{ 'SETTINGS.GENERAL_SUBTITLE' | translate }}</p>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="setting-item">
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.LANGUAGE' | translate }}</h4>
|
|
<p>{{ 'SETTINGS.LANGUAGE_DESCRIPTION' | translate }}</p>
|
|
</div>
|
|
<div class="setting-item__control">
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
|
<mat-label>{{
|
|
'SETTINGS.VIDEO_PLAYER_PLACEHOLDER' | translate
|
|
}}</mat-label>
|
|
<mat-select
|
|
formControlName="language"
|
|
data-test-id="select-language"
|
|
>
|
|
@for (
|
|
language of languageEnum() | keyvalue;
|
|
track language
|
|
) {
|
|
<mat-option
|
|
[value]="language.value"
|
|
[attr.data-test-id]="language.value"
|
|
>{{
|
|
'LANGUAGES.' + language.key | translate
|
|
}}</mat-option
|
|
>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-item setting-item--full-control">
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.THEME' | translate }}</h4>
|
|
<p>{{ 'SETTINGS.THEME_DESCRIPTION' | translate }}</p>
|
|
</div>
|
|
<div class="setting-item__control">
|
|
<div
|
|
class="theme-switcher"
|
|
role="radiogroup"
|
|
[attr.aria-label]="'SETTINGS.THEME' | translate"
|
|
data-test-id="select-theme"
|
|
>
|
|
@for (themeOption of themeOptions(); track themeOption.value) {
|
|
<button
|
|
type="button"
|
|
class="theme-switcher__option"
|
|
[class.theme-switcher__option--selected]="
|
|
form().value.theme === themeOption.value
|
|
"
|
|
[attr.aria-checked]="
|
|
form().value.theme === themeOption.value
|
|
"
|
|
[attr.data-test-id]="themeOption.value"
|
|
role="radio"
|
|
(click)="selectTheme.emit(themeOption.value)"
|
|
>
|
|
<mat-icon>{{ themeOption.icon }}</mat-icon>
|
|
<span>{{ themeOption.labelKey | translate }}</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-item setting-item--full-control">
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.COVER_SIZE' | translate }}</h4>
|
|
<p>{{ 'SETTINGS.COVER_SIZE_DESCRIPTION' | translate }}</p>
|
|
</div>
|
|
<div class="setting-item__control">
|
|
<div
|
|
class="theme-switcher"
|
|
role="radiogroup"
|
|
[attr.aria-label]="'SETTINGS.COVER_SIZE' | translate"
|
|
data-test-id="select-cover-size"
|
|
>
|
|
@for (option of coverSizeOptions(); track option.value) {
|
|
<button
|
|
type="button"
|
|
class="theme-switcher__option"
|
|
[class.theme-switcher__option--selected]="
|
|
form().value.coverSize === option.value
|
|
"
|
|
[attr.aria-checked]="
|
|
form().value.coverSize === option.value
|
|
"
|
|
[attr.data-test-id]="'cover-size-' + option.value"
|
|
role="radio"
|
|
(click)="selectCoverSize.emit(option.value)"
|
|
>
|
|
<mat-icon>{{ option.icon }}</mat-icon>
|
|
<span>{{ option.labelKey | translate }}</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-item">
|
|
<div class="setting-item__meta">
|
|
<h4 id="cover-titles-label">
|
|
{{ 'SETTINGS.COVER_TITLES' | translate }}
|
|
</h4>
|
|
<p id="cover-titles-description">
|
|
{{ 'SETTINGS.COVER_TITLES_DESCRIPTION' | translate }}
|
|
</p>
|
|
</div>
|
|
<div class="setting-item__control setting-item__toggle">
|
|
<mat-checkbox
|
|
formControlName="showCoverTitles"
|
|
aria-labelledby="cover-titles-label"
|
|
aria-describedby="cover-titles-description"
|
|
data-test-id="cover-titles-toggle"
|
|
></mat-checkbox>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-item">
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.STARTUP_BEHAVIOR' | translate }}</h4>
|
|
<p>
|
|
{{ 'SETTINGS.STARTUP_BEHAVIOR_DESCRIPTION' | translate }}
|
|
</p>
|
|
</div>
|
|
<div class="setting-item__control">
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
|
<mat-label>{{
|
|
'SETTINGS.STARTUP_BEHAVIOR' | translate
|
|
}}</mat-label>
|
|
<mat-select
|
|
formControlName="startupBehavior"
|
|
data-test-id="select-startup-behavior"
|
|
>
|
|
@for (
|
|
option of startupBehaviorOptions();
|
|
track option.value
|
|
) {
|
|
<mat-option
|
|
[value]="option.value"
|
|
[attr.data-test-id]="option.value"
|
|
>
|
|
{{ option.labelKey | translate }}
|
|
</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
</div>
|
|
|
|
@if (supportsStartupWindowMode()) {
|
|
<div class="setting-item" data-test-id="startup-window-mode-setting">
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.STARTUP_WINDOW_MODE' | translate }}</h4>
|
|
<p>
|
|
{{ 'SETTINGS.STARTUP_WINDOW_MODE_DESCRIPTION' | translate }}
|
|
</p>
|
|
</div>
|
|
<div class="setting-item__control">
|
|
<mat-form-field appearance="outline" subscriptSizing="dynamic">
|
|
<mat-label>{{
|
|
'SETTINGS.STARTUP_WINDOW_MODE' | translate
|
|
}}</mat-label>
|
|
<mat-select
|
|
formControlName="startupWindowMode"
|
|
data-test-id="select-startup-window-mode"
|
|
>
|
|
@for (
|
|
option of startupWindowModeOptions();
|
|
track option.value
|
|
) {
|
|
<mat-option
|
|
[value]="option.value"
|
|
[attr.data-test-id]="
|
|
'startup-window-mode-' + option.value
|
|
"
|
|
>
|
|
{{ option.labelKey | translate }}
|
|
</mat-option>
|
|
}
|
|
</mat-select>
|
|
</mat-form-field>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div class="setting-item">
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.SHOW_CAPTIONS' | translate }}</h4>
|
|
<p>
|
|
{{ 'SETTINGS.SHOW_CAPTIONS_DESCRIPTION' | translate }}
|
|
</p>
|
|
</div>
|
|
<div class="setting-item__control setting-item__toggle">
|
|
<mat-checkbox formControlName="showCaptions"></mat-checkbox>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-item">
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.STRIP_COUNTRY_PREFIX' | translate }}</h4>
|
|
<p>
|
|
{{ 'SETTINGS.STRIP_COUNTRY_PREFIX_DESCRIPTION' | translate }}
|
|
</p>
|
|
</div>
|
|
<div class="setting-item__control setting-item__toggle">
|
|
<mat-checkbox formControlName="stripCountryPrefix"></mat-checkbox>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
@if (supportsPortalConnectivityGuard()) {
|
|
<section
|
|
class="settings-group"
|
|
[formGroup]="form()"
|
|
data-test-id="portal-connectivity-setting"
|
|
>
|
|
<header class="settings-group__header">
|
|
<div class="settings-group__header-icon">
|
|
<mat-icon>hub</mat-icon>
|
|
</div>
|
|
<div class="settings-group__header-text">
|
|
<h3>{{ 'SETTINGS.PORTAL_CONNECTIONS' | translate }}</h3>
|
|
</div>
|
|
</header>
|
|
<div class="setting-item">
|
|
<div class="setting-item__meta">
|
|
<h4 id="portal-connectivity-label">
|
|
{{ 'SETTINGS.PORTAL_CONNECTIVITY_GUARD' | translate }}
|
|
</h4>
|
|
<p id="portal-connectivity-description">
|
|
{{
|
|
'SETTINGS.PORTAL_CONNECTIVITY_GUARD_DESCRIPTION'
|
|
| translate
|
|
}}
|
|
</p>
|
|
@if (form().get('portalConnectivityGuard')?.value === false) {
|
|
<p>
|
|
{{
|
|
'SETTINGS.PORTAL_CONNECTIVITY_GUARD_DISABLED'
|
|
| translate
|
|
}}
|
|
</p>
|
|
}
|
|
</div>
|
|
<div class="setting-item__control setting-item__toggle">
|
|
<mat-checkbox
|
|
formControlName="portalConnectivityGuard"
|
|
aria-labelledby="portal-connectivity-label"
|
|
aria-describedby="portal-connectivity-description"
|
|
data-test-id="portal-connectivity-toggle"
|
|
></mat-checkbox>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
}
|