mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
feat(workspace): route platform Back gestures to the header Back (#1943)
* feat(workspace): route platform Back gestures to the header Back The mouse Back button, Alt+Left (Cmd+[ on macOS) and, in Electron, the window's app-command browser-backward (Windows/Linux) and trackpad swipe (macOS) now run the workspace header's Back, one step each. - WorkspaceBackGesturesService (shell-provided) dispatches every gesture through WorkspaceBackNavigationService.goBack(). A gesture is swallowed while a dialog, menu, the phone drawer or a fullscreen player is up, and left to the browser only where the header has no Back. - The chord is a navigate-back entry in the shortcuts dialog, handled in a capture-phase listener of WorkspaceKeyboardShortcutsService; ignored while typing, repeats swallowed. - Electron forwards app-command and swipe over the push-only channel WINDOW:BACK_REQUESTED (preload onWindowBackRequested), gated in the renderer on RuntimeCapabilitiesService.supportsWindowBackGestures. - A mouse press that also arrives as app-command goes back once, however long the button is held; Electron's own Chromium history step on the mouse button is always prevented. - No Forward binding. Docs: workspace-shell (Platform gestures), portal-detail-navigation, electron-security, UI guidelines, README shortcuts table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(workspace): drop NgZone from the Back gestures service Master runs zoneless change detection and no longer uses NgZone; the bridge callback calls goBack() directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): keep Back gestures where the Fullscreen API is missing iPhone Safari has no unprefixed document.fullscreenElement; the undefined value read as "a player is fullscreen" and blocked every gesture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(workspace): keep a held Back chord's repeats swallowed after Back Repeats asked for the current result, so once the first press reached a page with no Back left they were released to the browser, which could take another history step. Repeats now follow the chord's first press until a key is released. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(electron): one Back per press of the Linux Browser Back key On Linux Electron raises app-command browser-backward for every unhandled event of the Browser Back key (NativeWindowViews:: HandleKeyboardEvent), keydown and keyup alike, so one press went back twice. The main process now consumes that key in before-input-event and requests Back once per press; mouse Back buttons still arrive as app-command. 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>
This commit is contained in:
44 files changed
+1758
-8
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import type { CDPSession, Page } from '@playwright/test';
|
||||
import { expect, test } from './fixtures';
|
||||
import {
|
||||
addXtreamPortal,
|
||||
@@ -49,6 +49,41 @@ async function openAsFirstEntry(page: Page, url: string): Promise<Page> {
|
||||
return firstEntryPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Back chord for the platform the page reports (the web project emulates
|
||||
* Windows): Cmd+[ on macOS, Alt+← elsewhere.
|
||||
*/
|
||||
async function backChord(page: Page): Promise<string> {
|
||||
const isMac = await page.evaluate(() =>
|
||||
/Mac|iPhone|iPad|iPod/i.test(
|
||||
(navigator as Navigator & { userAgentData?: { platform?: string } })
|
||||
.userAgentData?.platform || navigator.userAgent
|
||||
)
|
||||
);
|
||||
return isMac ? 'Meta+BracketLeft' : 'Alt+ArrowLeft';
|
||||
}
|
||||
|
||||
/** One mouse Back press through Chromium's input pipeline. */
|
||||
async function pressMouseBack(cdp: CDPSession, page: Page): Promise<void> {
|
||||
const box = await page.locator('main').first().boundingBox();
|
||||
const x = Math.round((box?.x ?? 0) + (box?.width ?? 400) / 2);
|
||||
const y = Math.round((box?.y ?? 0) + (box?.height ?? 400) / 2);
|
||||
for (const type of ['mousePressed', 'mouseReleased'] as const) {
|
||||
await cdp.send('Input.dispatchMouseEvent', {
|
||||
type,
|
||||
x,
|
||||
y,
|
||||
button: 'back',
|
||||
buttons: type === 'mousePressed' ? 8 : 0,
|
||||
clickCount: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function historyIndex(page: Page): Promise<number> {
|
||||
return page.evaluate(() => window.navigation?.currentEntry?.index ?? -1);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
await request.post(`${MOCK_SERVER}/reset`);
|
||||
await page.goto('/');
|
||||
@@ -82,6 +117,35 @@ test('@web @xtream the in-portal search page reached in the app goes back in his
|
||||
await page.waitForURL(/\/workspace\/dashboard$/);
|
||||
});
|
||||
|
||||
test('@web @xtream the mouse Back button and the Back chord go back one step', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.getByRole('link', { name: 'Series', exact: true }).click();
|
||||
await page.waitForURL(/\/series$/);
|
||||
const listUrl = page.url();
|
||||
const listIndex = await historyIndex(page);
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
|
||||
// The workspace runs Back and keeps the browser from its own step, which
|
||||
// would go back a second time.
|
||||
for (const gesture of [
|
||||
() => pressMouseBack(cdp, page),
|
||||
async () => page.keyboard.press(await backChord(page)),
|
||||
]) {
|
||||
await page.locator('.category-content-layout mat-card').first().click();
|
||||
await page.waitForURL(/\/series\/[^/]+\/[^/]+$/);
|
||||
await expect(page.locator('app-portal-detail-shell')).toBeVisible();
|
||||
|
||||
await gesture();
|
||||
await page.waitForURL(listUrl);
|
||||
await page.evaluate(
|
||||
() => new Promise((resolve) => setTimeout(resolve, 400))
|
||||
);
|
||||
expect(page.url()).toBe(listUrl);
|
||||
expect(await historyIndex(page)).toBe(listIndex);
|
||||
}
|
||||
});
|
||||
|
||||
for (const { name, path, selector, parent } of [
|
||||
{
|
||||
name: 'movie Discover',
|
||||
|
||||
Reference in new issue
Block a user