mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
* 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>
239 lines
10 KiB
HTML
239 lines
10 KiB
HTML
<section class="settings-group" id="epg" [formGroup]="form()">
|
|
<header class="settings-group__header">
|
|
<div class="settings-group__header-icon">
|
|
<mat-icon>list_alt</mat-icon>
|
|
</div>
|
|
<div class="settings-group__header-text">
|
|
<h3>{{ 'SETTINGS.EPG_SOURCES' | translate }}</h3>
|
|
<p>{{ 'SETTINGS.EPG_SOURCES_DESCRIPTION' | translate }}</p>
|
|
</div>
|
|
</header>
|
|
|
|
<div
|
|
class="setting-item setting-item--full-control"
|
|
data-setting-id="epg-view-mode"
|
|
>
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.EPG_VIEW_MODE' | translate }}</h4>
|
|
<p>{{ 'SETTINGS.EPG_VIEW_MODE_DESCRIPTION' | translate }}</p>
|
|
</div>
|
|
<div class="setting-item__control">
|
|
<div
|
|
class="theme-switcher"
|
|
role="radiogroup"
|
|
[attr.aria-label]="'SETTINGS.EPG_VIEW_MODE' | translate"
|
|
data-test-id="select-epg-view-mode"
|
|
>
|
|
@for (option of epgViewModeOptions(); track option.value) {
|
|
<button
|
|
type="button"
|
|
class="theme-switcher__option"
|
|
[class.theme-switcher__option--selected]="
|
|
form().value.epgViewMode === option.value
|
|
"
|
|
[attr.aria-checked]="
|
|
form().value.epgViewMode === option.value
|
|
"
|
|
[attr.data-test-id]="'epg-view-mode-' + option.value"
|
|
role="radio"
|
|
(click)="selectEpgViewMode.emit(option.value)"
|
|
>
|
|
<mat-icon>{{ option.icon }}</mat-icon>
|
|
<span>{{ option.labelKey | translate }}</span>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="setting-item setting-item--full-control"
|
|
data-setting-id="epg-offset"
|
|
>
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.EPG_OFFSET_TITLE' | translate }}</h4>
|
|
<p>{{ 'SETTINGS.EPG_OFFSET_HINT' | translate }}</p>
|
|
</div>
|
|
<div class="setting-item__control">
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>{{
|
|
'SETTINGS.EPG_OFFSET_LABEL' | translate
|
|
}}</mat-label>
|
|
<input
|
|
matInput
|
|
type="number"
|
|
min="-720"
|
|
max="720"
|
|
step="15"
|
|
formControlName="epgOffsetMinutes"
|
|
data-test-id="epg-offset-minutes"
|
|
/>
|
|
<mat-error>{{
|
|
'SETTINGS.EPG_OFFSET_ERROR' | translate
|
|
}}</mat-error>
|
|
</mat-form-field>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="setting-item setting-item--full-control"
|
|
data-setting-id="epg-sources"
|
|
>
|
|
<div class="setting-item__meta" data-test-id="epg-source-formats">
|
|
<h4>{{ 'SETTINGS.EPG_SOURCE_LIST_TITLE' | translate }}</h4>
|
|
<p>{{ 'SETTINGS.EPG_SOURCE_FORMATS_HINT' | translate }}</p>
|
|
<p class="epg-sources__formats-examples">
|
|
<span>{{
|
|
'SETTINGS.EPG_SOURCE_FORMATS_EXAMPLES' | translate
|
|
}}</span>
|
|
<code>https://example.org/guide.xml.gz</code>
|
|
<code>/home/you/epg/guide.xml</code>
|
|
<code>C:\epg\guide.xml.gz</code>
|
|
<code>file:///…/guide.xml</code>
|
|
</p>
|
|
</div>
|
|
<div class="setting-item__control">
|
|
<ng-container formArrayName="epgUrl">
|
|
@if (epgUrl().length === 0) {
|
|
<div
|
|
class="epg-sources-empty"
|
|
data-test-id="epg-sources-empty"
|
|
>
|
|
<mat-icon class="epg-sources-empty__icon"
|
|
>event_busy</mat-icon
|
|
>
|
|
<h4 class="epg-sources-empty__title">
|
|
{{ 'SETTINGS.EPG_EMPTY_TITLE' | translate }}
|
|
</h4>
|
|
<p class="epg-sources-empty__hint">
|
|
{{ 'SETTINGS.EPG_EMPTY_HINT' | translate }}
|
|
</p>
|
|
</div>
|
|
} @else {
|
|
<div class="epg-sources">
|
|
@for (_ of epgUrl().controls; track _; let i = $index) {
|
|
<div class="epg-source-row">
|
|
<mat-form-field
|
|
appearance="outline"
|
|
subscriptSizing="dynamic"
|
|
class="w-full"
|
|
>
|
|
<mat-label>{{
|
|
'SETTINGS.EPG_URL_PLACEHOLDER'
|
|
| translate
|
|
}}</mat-label>
|
|
<input
|
|
matInput
|
|
type="text"
|
|
spellcheck="false"
|
|
[formControlName]="i"
|
|
#epgField
|
|
/>
|
|
<mat-error>{{
|
|
'SETTINGS.EPG_URL_ERROR' | translate
|
|
}}</mat-error>
|
|
</mat-form-field>
|
|
@if (epgField.value) {
|
|
<app-epg-source-status
|
|
[url]="epgField.value"
|
|
/>
|
|
}
|
|
@if (canBrowseFiles()) {
|
|
<button
|
|
mat-icon-button
|
|
[matTooltip]="
|
|
'SETTINGS.BROWSE_EPG_FILE'
|
|
| translate
|
|
"
|
|
[attr.aria-label]="
|
|
'SETTINGS.BROWSE_EPG_FILE'
|
|
| translate
|
|
"
|
|
type="button"
|
|
data-test-id="epg-source-browse"
|
|
(click)="browseEpgSource.emit(i)"
|
|
>
|
|
<mat-icon>folder_open</mat-icon>
|
|
</button>
|
|
}
|
|
<button
|
|
mat-icon-button
|
|
[matTooltip]="
|
|
'SETTINGS.REFRESH_EPG' | translate
|
|
"
|
|
type="button"
|
|
data-test-id="epg-source-refresh"
|
|
[disabled]="epgField.value === ''"
|
|
(click)="refreshEpg.emit(epgUrl().value[i])"
|
|
>
|
|
<mat-icon>refresh</mat-icon>
|
|
</button>
|
|
<button
|
|
mat-icon-button
|
|
[matTooltip]="
|
|
'SETTINGS.REMOVE_EPG_SOURCE' | translate
|
|
"
|
|
type="button"
|
|
data-test-id="epg-source-remove"
|
|
(click)="removeEpgSource.emit(i)"
|
|
>
|
|
<mat-icon>remove</mat-icon>
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</ng-container>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="setting-item" data-setting-id="prefer-uploaded-epg">
|
|
<div class="setting-item__meta">
|
|
<h4>{{ 'SETTINGS.PREFER_UPLOADED_EPG_TITLE' | translate }}</h4>
|
|
<p>{{ 'SETTINGS.PREFER_UPLOADED_EPG_HINT' | translate }}</p>
|
|
</div>
|
|
<div class="setting-item__control setting-item__toggle">
|
|
<mat-checkbox
|
|
formControlName="preferUploadedEpgOverXtream"
|
|
data-test-id="toggle-prefer-uploaded-epg"
|
|
></mat-checkbox>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="settings-group__footer">
|
|
<div class="inline-actions">
|
|
<button mat-button type="button" (click)="addEpgSource.emit()">
|
|
<mat-icon>add</mat-icon>
|
|
{{ 'SETTINGS.ADD_EPG_SOURCE' | translate }}
|
|
</button>
|
|
<button
|
|
mat-button
|
|
type="button"
|
|
[disabled]="epgUrl().length === 0 || isClearingEpgData()"
|
|
(click)="refreshAllEpg.emit()"
|
|
>
|
|
<mat-icon>refresh</mat-icon>
|
|
{{ 'SETTINGS.REFRESH_ALL_EPG' | translate }}
|
|
</button>
|
|
<button
|
|
mat-button
|
|
color="warn"
|
|
type="button"
|
|
[disabled]="isClearingEpgData()"
|
|
(click)="clearEpgData.emit()"
|
|
>
|
|
@if (isClearingEpgData()) {
|
|
<mat-progress-spinner
|
|
class="button-spinner"
|
|
diameter="16"
|
|
mode="indeterminate"
|
|
/>
|
|
{{ 'SETTINGS.CLEARING_EPG_DATA' | translate }}
|
|
} @else {
|
|
{{ 'SETTINGS.CLEAR_EPG_DATA' | translate }}
|
|
}
|
|
</button>
|
|
</div>
|
|
</footer>
|
|
</section>
|