mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
* fix(player): keyboard focus, contrast and ARIA for controls and settings
Dock and settings-panel icon buttons draw a 2px --pc-text ring on
:focus-visible, and Material's theme-coloured focus layer is off, so
keyboard focus shows on video in the light theme too. A focused selected
subtitle swatch now differs from one that is only selected.
Settings headings read --pc-text-secondary on denser glass
(--pc-glass-bg-dense, 0.86): 4.5:1 or more over mid-grey and white
frames. They wrap (overflow-wrap: anywhere, hyphens: auto), so long
German and Russian headings stay inside the sheet's heading column.
The settings panel is now radio groups only (SettingsRadioGroupDirective
over a CDK FocusKeyManager): one Tab stop per group on the checked option,
arrows, Home and End move focus without applying, and Space/Enter checks.
The dialog and its groups are named by real h2/h3/h4 headings, the
load-file action sits outside the subtitle radio group, the subtitle and
speed chips carry their value in their name ("Subtitles: English"), and
tune has aria-haspopup="dialog".
Adds a web E2E for the keyboard path in both themes with an axe check on
the open panel, and de/ru sheet heading wrapping; axe-core is a new dev
dependency for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): arrows check settings radios; subtitle chip reads On
Review follow-up:
- Arrow keys, Home and End now check the settings radio they reach, as a
native radio group does (the directive clicks it, so the template's
handler applies the choice); an option the engine already reports as
checked is not applied again.
- With subtitles on but no track marked selected yet (the engine can
report the switch before the track list), the subtitle chip reads and
announces "On" (new SUBTITLES_ON key, 19 locales) instead of "Off".
- The swatch row has 4px padding on every side, so the outer focus ring
is not clipped at the scroll edge of the panel body.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(player): re-apply a settings radio while a switch is pending
The arrow-key check skipped any option the engine still reported as
checked. Arrowing from audio track A to B and back to A before the engine
confirmed B therefore sent no command for A, and playback ended on B with
focus on A. An arrow move always lands on an option other than the last
one applied, so it now applies unconditionally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
435 lines
18 KiB
HTML
435 lines
18 KiB
HTML
@if (mode() === 'sheet') {
|
|
<div class="player-settings__grip" aria-hidden="true"></div>
|
|
}
|
|
<div class="player-settings__header">
|
|
<h2 class="player-settings__title" [id]="headingId('title')">
|
|
{{ title() }}
|
|
</h2>
|
|
<button
|
|
mat-icon-button
|
|
type="button"
|
|
class="player-settings__close"
|
|
data-test-id="player-settings-close"
|
|
(click)="closeRequested.emit()"
|
|
[attr.aria-label]="'EMBEDDED_MPV.PLAYER.CLOSE_SETTINGS' | translate"
|
|
[matTooltip]="'EMBEDDED_MPV.PLAYER.CLOSE_SETTINGS' | translate"
|
|
>
|
|
<mat-icon>close</mat-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="player-settings__body">
|
|
@if (groups().audio) {
|
|
<section
|
|
class="player-settings__group"
|
|
[class.player-settings__group--focused]="isFocused('audio')"
|
|
data-group="audio"
|
|
data-test-id="player-settings-audio"
|
|
>
|
|
<h3 class="player-settings__heading" [id]="headingId('audio')">
|
|
<span
|
|
class="player-settings__dot player-settings__dot--cyan"
|
|
aria-hidden="true"
|
|
></span>
|
|
{{ 'EMBEDDED_MPV.PLAYER.AUDIO_TRACKS' | translate }}
|
|
</h3>
|
|
<div
|
|
class="player-settings__options"
|
|
appSettingsRadioGroup
|
|
[attr.aria-labelledby]="headingId('audio')"
|
|
>
|
|
@for (track of state().audioTracks; track track.id) {
|
|
<button
|
|
type="button"
|
|
class="player-settings__option"
|
|
[appSettingsRadio]="track.selected"
|
|
[class.player-settings__option--selected]="
|
|
track.selected
|
|
"
|
|
(click)="selection().audioTrack(track.id)"
|
|
>
|
|
<span>{{ track.label }}</span>
|
|
@if (track.selected) {
|
|
<mat-icon>check</mat-icon>
|
|
}
|
|
</button>
|
|
}
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@if (groups().subtitles) {
|
|
<section
|
|
class="player-settings__group"
|
|
[class.player-settings__group--focused]="isFocused('subtitles')"
|
|
data-group="subtitles"
|
|
data-test-id="player-settings-subtitles"
|
|
>
|
|
<h3 class="player-settings__heading" [id]="headingId('subtitles')">
|
|
<span
|
|
class="player-settings__dot player-settings__dot--cyan"
|
|
aria-hidden="true"
|
|
></span>
|
|
{{ 'EMBEDDED_MPV.PLAYER.SUBTITLES' | translate }}
|
|
</h3>
|
|
@if (state().subtitleTracks.length > 0) {
|
|
<div
|
|
class="player-settings__options"
|
|
appSettingsRadioGroup
|
|
[attr.aria-labelledby]="headingId('subtitles')"
|
|
>
|
|
@if (capabilities().subtitles) {
|
|
<button
|
|
type="button"
|
|
class="player-settings__option player-settings__option--default"
|
|
[appSettingsRadio]="!state().subtitlesEnabled"
|
|
[class.player-settings__option--selected]="
|
|
!state().subtitlesEnabled
|
|
"
|
|
(click)="selection().subtitleTrack(-1)"
|
|
>
|
|
<span>{{
|
|
'EMBEDDED_MPV.PLAYER.SUBTITLES_OFF' | translate
|
|
}}</span>
|
|
@if (!state().subtitlesEnabled) {
|
|
<mat-icon>check</mat-icon>
|
|
}
|
|
</button>
|
|
}
|
|
@for (track of state().subtitleTracks; track track.id) {
|
|
<button
|
|
type="button"
|
|
class="player-settings__option"
|
|
[appSettingsRadio]="track.selected"
|
|
[class.player-settings__option--selected]="
|
|
track.selected
|
|
"
|
|
(click)="selection().subtitleTrack(track.id)"
|
|
>
|
|
<span>{{ track.label }}</span>
|
|
@if (track.selected) {
|
|
<mat-icon>check</mat-icon>
|
|
}
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
@if (capabilities().externalSubtitles) {
|
|
<!-- An action, not a choice: kept out of the radio group. -->
|
|
<button
|
|
type="button"
|
|
class="player-settings__option player-settings__option--action"
|
|
data-test-id="player-controls-load-subtitle"
|
|
(click)="loadExternalSubtitle()"
|
|
>
|
|
<span>{{
|
|
'EMBEDDED_MPV.PLAYER.LOAD_SUBTITLE_FILE' | translate
|
|
}}</span>
|
|
<mat-icon>upload_file</mat-icon>
|
|
</button>
|
|
}
|
|
|
|
@if (capabilities().subtitleDelay) {
|
|
<div
|
|
class="player-settings__subsection"
|
|
data-test-id="player-controls-subtitle-delay"
|
|
>
|
|
<h4
|
|
class="player-settings__subheading"
|
|
[id]="headingId('subtitle-delay')"
|
|
>
|
|
{{ 'EMBEDDED_MPV.PLAYER.SUBTITLE_DELAY' | translate }}
|
|
</h4>
|
|
<div
|
|
class="player-settings__delay-row"
|
|
role="group"
|
|
[attr.aria-labelledby]="headingId('subtitle-delay')"
|
|
>
|
|
<button
|
|
mat-icon-button
|
|
type="button"
|
|
(click)="
|
|
subtitleSettings().adjustDelay(
|
|
-subtitleDelayStep
|
|
)
|
|
"
|
|
[attr.aria-label]="
|
|
'EMBEDDED_MPV.PLAYER.SUBTITLE_DELAY_DECREASE'
|
|
| translate
|
|
"
|
|
[matTooltip]="
|
|
'EMBEDDED_MPV.PLAYER.SUBTITLE_DELAY_DECREASE'
|
|
| translate
|
|
"
|
|
>
|
|
<mat-icon>remove</mat-icon>
|
|
</button>
|
|
<span
|
|
class="player-settings__delay-value"
|
|
aria-live="polite"
|
|
>{{
|
|
subtitleDelayLabel(state().subtitleDelaySeconds)
|
|
}}</span
|
|
>
|
|
<button
|
|
mat-icon-button
|
|
type="button"
|
|
(click)="
|
|
subtitleSettings().adjustDelay(
|
|
subtitleDelayStep
|
|
)
|
|
"
|
|
[attr.aria-label]="
|
|
'EMBEDDED_MPV.PLAYER.SUBTITLE_DELAY_INCREASE'
|
|
| translate
|
|
"
|
|
[matTooltip]="
|
|
'EMBEDDED_MPV.PLAYER.SUBTITLE_DELAY_INCREASE'
|
|
| translate
|
|
"
|
|
>
|
|
<mat-icon>add</mat-icon>
|
|
</button>
|
|
<button
|
|
mat-icon-button
|
|
type="button"
|
|
[disabled]="state().subtitleDelaySeconds === 0"
|
|
(click)="subtitleSettings().resetDelay()"
|
|
[attr.aria-label]="
|
|
'EMBEDDED_MPV.PLAYER.SUBTITLE_DELAY_RESET'
|
|
| translate
|
|
"
|
|
[matTooltip]="
|
|
'EMBEDDED_MPV.PLAYER.SUBTITLE_DELAY_RESET'
|
|
| translate
|
|
"
|
|
>
|
|
<mat-icon>restart_alt</mat-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (capabilities().subtitleStyle) {
|
|
<div
|
|
class="player-settings__subsection"
|
|
data-test-id="player-controls-subtitle-style"
|
|
>
|
|
<h4
|
|
class="player-settings__subheading"
|
|
[id]="headingId('subtitle-size')"
|
|
>
|
|
{{ 'EMBEDDED_MPV.PLAYER.SUBTITLE_SIZE' | translate }}
|
|
</h4>
|
|
<div
|
|
class="player-settings__seg"
|
|
appSettingsRadioGroup
|
|
[attr.aria-labelledby]="headingId('subtitle-size')"
|
|
>
|
|
@for (
|
|
preset of subtitleSizePresets;
|
|
track preset.value
|
|
) {
|
|
<button
|
|
type="button"
|
|
class="player-settings__seg-item"
|
|
[appSettingsRadio]="
|
|
preset.value ===
|
|
state().subtitleStyle.sizePercent
|
|
"
|
|
[class.player-settings__seg-item--selected]="
|
|
preset.value ===
|
|
state().subtitleStyle.sizePercent
|
|
"
|
|
(click)="
|
|
subtitleSettings().setSize(preset.value)
|
|
"
|
|
>
|
|
{{ preset.label }}
|
|
</button>
|
|
}
|
|
</div>
|
|
<h4
|
|
class="player-settings__subheading"
|
|
[id]="headingId('subtitle-color')"
|
|
>
|
|
{{ 'EMBEDDED_MPV.PLAYER.SUBTITLE_COLOR' | translate }}
|
|
</h4>
|
|
<div
|
|
class="player-settings__swatches"
|
|
appSettingsRadioGroup
|
|
[attr.aria-labelledby]="headingId('subtitle-color')"
|
|
>
|
|
@for (
|
|
preset of subtitleColorPresets;
|
|
track preset.label
|
|
) {
|
|
<button
|
|
type="button"
|
|
class="player-settings__swatch"
|
|
[appSettingsRadio]="
|
|
preset.value === state().subtitleStyle.color
|
|
"
|
|
[class.player-settings__swatch--default]="
|
|
preset.value === null
|
|
"
|
|
[class.player-settings__swatch--selected]="
|
|
preset.value === state().subtitleStyle.color
|
|
"
|
|
[style.--swatch-color]="preset.value"
|
|
[attr.aria-label]="preset.label | translate"
|
|
[matTooltip]="preset.label | translate"
|
|
(click)="
|
|
subtitleSettings().setColor(preset.value)
|
|
"
|
|
></button>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</section>
|
|
}
|
|
|
|
@if (groups().quality) {
|
|
<section
|
|
class="player-settings__group"
|
|
[class.player-settings__group--focused]="isFocused('quality')"
|
|
data-group="quality"
|
|
data-test-id="player-settings-quality"
|
|
>
|
|
<h3 class="player-settings__heading" [id]="headingId('quality')">
|
|
<span
|
|
class="player-settings__dot player-settings__dot--violet"
|
|
aria-hidden="true"
|
|
></span>
|
|
{{ 'EMBEDDED_MPV.PLAYER.QUALITY' | translate }}
|
|
</h3>
|
|
<div
|
|
class="player-settings__options"
|
|
appSettingsRadioGroup
|
|
[attr.aria-labelledby]="headingId('quality')"
|
|
>
|
|
<button
|
|
type="button"
|
|
class="player-settings__option player-settings__option--default"
|
|
[appSettingsRadio]="state().qualityAutoEnabled"
|
|
[class.player-settings__option--selected]="
|
|
state().qualityAutoEnabled
|
|
"
|
|
(click)="selection().qualityLevel(-1)"
|
|
>
|
|
<span>{{
|
|
'EMBEDDED_MPV.PLAYER.QUALITY_AUTO' | translate
|
|
}}</span>
|
|
@if (state().qualityAutoEnabled) {
|
|
<mat-icon>check</mat-icon>
|
|
}
|
|
</button>
|
|
@for (level of state().qualityLevels; track level.id) {
|
|
<button
|
|
type="button"
|
|
class="player-settings__option"
|
|
[appSettingsRadio]="level.selected"
|
|
[class.player-settings__option--selected]="
|
|
level.selected
|
|
"
|
|
(click)="selection().qualityLevel(level.id)"
|
|
>
|
|
<span>{{ level.label }}</span>
|
|
@if (level.selected) {
|
|
<mat-icon>check</mat-icon>
|
|
}
|
|
</button>
|
|
}
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@if (groups().speed) {
|
|
<section
|
|
class="player-settings__group"
|
|
[class.player-settings__group--focused]="isFocused('speed')"
|
|
data-group="speed"
|
|
data-test-id="player-settings-speed"
|
|
>
|
|
<h3 class="player-settings__heading" [id]="headingId('speed')">
|
|
<span
|
|
class="player-settings__dot player-settings__dot--violet"
|
|
aria-hidden="true"
|
|
></span>
|
|
{{ 'EMBEDDED_MPV.PLAYER.PLAYBACK_SPEED' | translate }}
|
|
</h3>
|
|
<div
|
|
class="player-settings__seg"
|
|
appSettingsRadioGroup
|
|
[attr.aria-labelledby]="headingId('speed')"
|
|
>
|
|
@for (preset of state().speedPresets; track preset.value) {
|
|
<button
|
|
type="button"
|
|
class="player-settings__seg-item"
|
|
[appSettingsRadio]="
|
|
preset.value === state().playbackSpeed
|
|
"
|
|
[class.player-settings__seg-item--default]="
|
|
preset.value === 1
|
|
"
|
|
[class.player-settings__seg-item--selected]="
|
|
preset.value === state().playbackSpeed
|
|
"
|
|
(click)="selection().speed(preset.value)"
|
|
>
|
|
{{ preset.label }}
|
|
</button>
|
|
}
|
|
</div>
|
|
</section>
|
|
}
|
|
|
|
@if (groups().aspect) {
|
|
<section
|
|
class="player-settings__group"
|
|
[class.player-settings__group--focused]="isFocused('aspect')"
|
|
data-group="aspect"
|
|
data-test-id="player-settings-aspect"
|
|
>
|
|
<h3 class="player-settings__heading" [id]="headingId('aspect')">
|
|
<span
|
|
class="player-settings__dot player-settings__dot--neutral"
|
|
aria-hidden="true"
|
|
></span>
|
|
{{ 'EMBEDDED_MPV.PLAYER.ASPECT_RATIO' | translate }}
|
|
</h3>
|
|
<div
|
|
class="player-settings__seg"
|
|
appSettingsRadioGroup
|
|
[attr.aria-labelledby]="headingId('aspect')"
|
|
>
|
|
@for (preset of state().aspectPresets; track preset.value) {
|
|
<button
|
|
type="button"
|
|
class="player-settings__seg-item"
|
|
[appSettingsRadio]="
|
|
preset.value === state().aspectRatio
|
|
"
|
|
[class.player-settings__seg-item--default]="
|
|
preset.value === defaultAspect()
|
|
"
|
|
[class.player-settings__seg-item--selected]="
|
|
preset.value === state().aspectRatio
|
|
"
|
|
(click)="selection().aspect(preset.value)"
|
|
>
|
|
{{
|
|
preset.value === 'no'
|
|
? ('EMBEDDED_MPV.PLAYER.ASPECT_DEFAULT'
|
|
| translate)
|
|
: preset.label
|
|
}}
|
|
</button>
|
|
}
|
|
</div>
|
|
</section>
|
|
}
|
|
</div>
|