Files
iptvnator/apps/web/src/app/settings/settings-general-section.component.html
T
ea51cae3db feat(settings): search settings from the header and the command palette (#1714)
* feat(settings): search settings from the header and the command palette

The header search on the Settings page was shown but disabled. It now
searches a shared index of all 56 settings rows by translated title,
description and English synonyms, replaces the section page with ranked
results, and opens a result by scrolling to, focusing and briefly
highlighting its row. Enter opens the best match, and the section
navigation shows per-section match counts.

The command palette gains a "Settings" group that lists the best six
matches for a non-empty query, so any setting is one Ctrl/Cmd+K away.
Rows hidden by the current form state fall back to the control that
reveals them; rows the runtime cannot render are never returned.

The index ships through a new @iptvnator/workspace/shell/util/settings-search
sub-entrypoint so it stays out of the eager bundle, and a registry spec
keeps it in step with the section templates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(settings): let search reveals win over pending input and gate embedded MPV rows

- A reveal (result click, command palette, Enter) now cancels a search
  keystroke still waiting for its debounce, so its q navigation can no
  longer supersede the reveal and leave the results open.
- Embedded MPV extra options and auto-reconnect require a lazily probed
  embedded MPV capability; frame copy also needs frameCopyAvailable, so
  search never offers a row the settings page cannot render.
- Keyboard users keep a focus-visible ring on the revealed row after the
  highlight fades.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(workspace): wait for palette probes without Promise.allSettled

The web tsconfig lib predates Promise.allSettled; use Promise.all over
rejection-safe probes instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 07:42:53 +02:00

277 lines
11 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" data-setting-id="language">
<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"
data-setting-id="theme"
>
<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"
data-setting-id="cover-size"
>
<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" data-setting-id="cover-titles">
<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" data-setting-id="startup-behavior">
<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-setting-id="startup-window-mode"
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" data-setting-id="show-captions">
<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" data-setting-id="strip-country-prefix">
<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" data-setting-id="portal-connectivity-guard">
<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>
}