mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
fix(playback): address Up next card review feedback
- Offer the Up next card setting for Embedded MPV only under the frame-copy engine; native view never mounts the shared controls. - Hovering pauses the collapse delay instead of restarting it. - Document that the card stays visible when the controls auto-hide. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5f6e8b037f
commit
b69b1e3d74
8 files changed
+54
-16
No files matched your search
@@ -171,10 +171,7 @@ test.describe('@xtream inline series fullscreen', () => {
|
||||
});
|
||||
|
||||
// The corner button hides the card for this episode; bringing the
|
||||
// controls back does not bring it back. The card hides with idle
|
||||
// controls, so wake them first.
|
||||
await playerView.hover();
|
||||
await expect(card).toBeVisible();
|
||||
// controls back does not bring it back.
|
||||
await playerView
|
||||
.locator('[data-test-id="player-controls-up-next-close"]')
|
||||
.click();
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (supportsFullscreenChannelPanel()) {
|
||||
@if (supportsUpNextCard()) {
|
||||
<div
|
||||
class="setting-item"
|
||||
data-setting-id="player-up-next-card"
|
||||
@@ -159,7 +159,9 @@
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (supportsFullscreenChannelPanel()) {
|
||||
<div
|
||||
class="setting-item"
|
||||
data-setting-id="fullscreen-channel-panel"
|
||||
|
||||
@@ -409,6 +409,19 @@ describe('SettingsPlaybackSectionComponent', () => {
|
||||
expect(
|
||||
queryByTestId('fullscreen-channel-panel-setting')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('offers the up next card toggle for Embedded MPV only under the frame-copy engine', () => {
|
||||
// The native-view engine never mounts the shared controls.
|
||||
fixture.componentRef.setInput(
|
||||
'form',
|
||||
createForm(VideoPlayer.EmbeddedMpv)
|
||||
);
|
||||
fixture.detectChanges();
|
||||
expect(queryByTestId('player-up-next-card-setting')).toBeNull();
|
||||
|
||||
fixture.componentRef.setInput('frameCopyActive', true);
|
||||
fixture.detectChanges();
|
||||
expect(queryByTestId('player-up-next-card-setting')).not.toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,20 @@ export class SettingsPlaybackSectionComponent {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Up next card is part of the shared controls. Embedded MPV mounts
|
||||
* them only under the frame-copy engine; the native-view engine keeps
|
||||
* its own dock, where the toggle would control nothing.
|
||||
*/
|
||||
supportsUpNextCard(): boolean {
|
||||
const value = this.form().value;
|
||||
return (
|
||||
(this.isWebPlayerSelected() &&
|
||||
value.webPlayerSharedControls !== false) ||
|
||||
(value.player === VideoPlayer.EmbeddedMpv && this.frameCopyActive())
|
||||
);
|
||||
}
|
||||
|
||||
isExternalPlayerSelected(): boolean {
|
||||
const player = this.form().value.player;
|
||||
return player === VideoPlayer.MPV || player === VideoPlayer.VLC;
|
||||
|
||||
@@ -424,8 +424,9 @@ routes such a request through the Up Next rail selection
|
||||
(`upNextEpisodeSelected`) whenever `seriesNavigation.canNext` is false, which
|
||||
plays the next season's first episode; either path keeps fullscreen exactly
|
||||
like the transport button.
|
||||
The card is a glass surface that shows and hides with the controls; the
|
||||
compact dock uses a smaller variant without the trailing icon. After
|
||||
The card is a glass surface that stays visible when the controls auto-hide
|
||||
(`showControls` is the host's input, not the idle state); the compact dock
|
||||
uses a smaller variant without the trailing icon. After
|
||||
`UP_NEXT_COLLAPSE_DELAY_MS` (10 s, paused while hovered) the card asks to
|
||||
collapse and `ControlsUpNext.collapse()` turns it into a one-line pill
|
||||
(label and countdown) for the rest of the episode. A corner close button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Glass card, bottom-right above the dock, inside the controls host (the
|
||||
// `player-controls` container) whose `--pc-*` palette it reads. It shows and
|
||||
// hides with the controls, collapses to a pill once seen, and the corner
|
||||
// close button dismisses it for the episode.
|
||||
// `player-controls` container) whose `--pc-*` palette it reads. It does not
|
||||
// fade with idle controls; instead it collapses to a pill once seen, and the
|
||||
// corner close button dismisses it for the episode.
|
||||
:host {
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
|
||||
@@ -152,7 +152,7 @@ describe('PlayerUpNextCardComponent', () => {
|
||||
expect(documentListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('asks to collapse after a while, but not while hovered', () => {
|
||||
it('asks to collapse after a while, pausing the count while hovered', () => {
|
||||
jest.useFakeTimers();
|
||||
fixture.destroy();
|
||||
fixture = TestBed.createComponent(PlayerUpNextCardComponent);
|
||||
@@ -167,14 +167,16 @@ describe('PlayerUpNextCardComponent', () => {
|
||||
const collapse = jest.fn();
|
||||
fixture.componentInstance.collapseRequested.subscribe(collapse);
|
||||
|
||||
jest.advanceTimersByTime(UP_NEXT_COLLAPSE_DELAY_MS - 1000);
|
||||
fixture.nativeElement.dispatchEvent(new MouseEvent('mouseenter'));
|
||||
fixture.detectChanges();
|
||||
jest.advanceTimersByTime(UP_NEXT_COLLAPSE_DELAY_MS * 2);
|
||||
expect(collapse).not.toHaveBeenCalled();
|
||||
|
||||
// Leaving resumes the remaining second instead of a fresh delay.
|
||||
fixture.nativeElement.dispatchEvent(new MouseEvent('mouseleave'));
|
||||
fixture.detectChanges();
|
||||
jest.advanceTimersByTime(UP_NEXT_COLLAPSE_DELAY_MS - 1);
|
||||
jest.advanceTimersByTime(999);
|
||||
expect(collapse).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(1);
|
||||
expect(collapse).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -18,8 +18,8 @@ export const UP_NEXT_COLLAPSE_DELAY_MS = 10_000;
|
||||
* The "Up next" card in the player's bottom-right corner: thumbnail, the
|
||||
* time left, the next episode's label and title. One click plays it through
|
||||
* the host's ordinary next-episode path, so fullscreen survives. After a few
|
||||
* seconds it asks to collapse into a pill (never while hovered), and the
|
||||
* close button or Escape dismisses it for this episode.
|
||||
* seconds of being seen (hovering pauses the count) it asks to collapse into
|
||||
* a pill, and the close button or Escape dismisses it for this episode.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-player-up-next-card',
|
||||
@@ -46,6 +46,8 @@ export class PlayerUpNextCardComponent {
|
||||
readonly collapseRequested = output<void>();
|
||||
|
||||
readonly hovered = signal(false);
|
||||
/** Collapse delay still owed; hovering pauses it rather than resetting. */
|
||||
private collapseRemainingMs = UP_NEXT_COLLAPSE_DELAY_MS;
|
||||
|
||||
/** Whole seconds under a minute, otherwise whole minutes (at least 1). */
|
||||
readonly countdown = computed(() => {
|
||||
@@ -73,11 +75,18 @@ export class PlayerUpNextCardComponent {
|
||||
if (this.collapsed() || this.hovered()) {
|
||||
return;
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
const timer = setTimeout(
|
||||
() => this.collapseRequested.emit(),
|
||||
UP_NEXT_COLLAPSE_DELAY_MS
|
||||
this.collapseRemainingMs
|
||||
);
|
||||
onCleanup(() => clearTimeout(timer));
|
||||
onCleanup(() => {
|
||||
clearTimeout(timer);
|
||||
this.collapseRemainingMs = Math.max(
|
||||
0,
|
||||
this.collapseRemainingMs - (Date.now() - startedAt)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user