Files
iptvnator/libs/ui/playback/src/lib/player-controls/player-timeline.component.html
T
4grayandClaude Opus 5.5 fb62b14feb fix(playback): keep player controls in the style budget, fix cross-season Up next and dialog focus
CI: the production build failed the 15 kB component style budget
(player-controls.component.scss grew to 16.5 kB minified).
- Extract the timeline row into app-player-timeline with its own
  stylesheet; scrub events still go back to the controls component.
- The settings panel and Up next card place themselves from their own
  :host styles.
- Drop dead --mdc-icon-button-* tokens (Material 22 reads --mat-* only)
  and the popover-anchor rule the action cluster no longer needs.
Largest stylesheet is now 11.7 kB, below the 12.5 kB warning.

Review:
- Up next at a season's last episode: the card follows the host's item
  instead of the season-local canNextEpisode, emits nextEpisodeRequested
  directly, and PortalInlinePlayerComponent routes it through the Up Next
  rail selection when seriesNavigation.canNext is false.
- Settings dialog focus: opened from the keyboard it takes focus, closing
  with focus inside returns it to `tune`, and the dock is inert while the
  compact sheet replaces it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 23:59:10 +02:00

99 lines
3.4 KiB
HTML

<span class="player-controls__time player-controls__time--current">{{
formatTime(timeline().value())
}}</span>
@if (seekable()) {
<div
class="player-controls__timeline-bar"
[class.player-controls__timeline-bar--disabled]="!canSeek()"
(pointermove)="hover().move($event)"
(pointerleave)="hover().clear()"
>
<div class="player-controls__timeline-track" aria-hidden="true">
@for (segment of timeline().segments(); track $index) {
<div
class="player-controls__timeline-segment"
[class.player-controls__timeline-segment--titled]="
segment.title !== null
"
[style.left.%]="segment.startPercent"
[style.width]="segment.width"
[attr.data-segment-title]="segment.title"
>
<div
class="player-controls__timeline-fill"
[style.width.%]="timeline().fillPercent(segment)"
></div>
</div>
}
</div>
@if (canSeek()) {
<div
class="player-controls__timeline-knob"
[style.left.%]="timeline().progress()"
aria-hidden="true"
></div>
}
@if (hover().percent() !== null) {
<div
class="player-controls__timeline-marker"
[style.left.%]="hover().percent()"
aria-hidden="true"
></div>
<div
class="player-controls__timeline-label"
[style.--hover-x.%]="hover().percent()"
data-test-id="player-controls-timeline-label"
aria-hidden="true"
>
{{ hover().label() }}
</div>
}
<input
class="player-controls__slider player-controls__slider--timeline"
[style.--slider-progress.%]="timeline().progress()"
type="range"
min="0"
[max]="timeline().duration()"
[value]="timeline().value()"
[disabled]="!canSeek()"
[attr.aria-label]="
'EMBEDDED_MPV.PLAYER.PLAYBACK_POSITION' | translate
"
[attr.aria-valuetext]="formatTime(timeline().value())"
(input)="scrubInput.emit($event)"
(change)="scrubCommit.emit($event)"
/>
</div>
} @else {
<div class="player-controls__timeline-spacer"></div>
}
@if (canSeek() && remaining(); as remaining) {
<span
class="player-controls__time player-controls__time--end"
data-test-id="player-controls-remaining-time"
>{{ remaining }}</span
>
} @else if (isLive()) {
<span
class="player-controls__live-badge"
[attr.aria-label]="'EMBEDDED_MPV.PLAYER.LIVE_STREAM' | translate"
>
<span class="player-controls__live-dot" aria-hidden="true"></span>
LIVE
</span>
} @else {
<span class="player-controls__time player-controls__time--end">--:--</span>
}
@if (recordingStatus(); as status) {
<div
class="player-controls__recording-status"
[class.player-controls__recording-status--active]="recording()"
[attr.aria-live]="recording() ? 'off' : 'polite'"
>
<mat-icon>{{
recording() ? 'fiber_manual_record' : 'check_circle'
}}</mat-icon>
<span>{{ status }}</span>
</div>
}