mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
feat(shell): zoom shortcuts on Windows and Linux (#1109)
Cmd/Ctrl and +/−/0 (numpad included) now zoom the app on every platform. Windows/Linux run without a menu (`setMenu(null)`), so the shortcuts are a renderer key binding in `WorkspaceKeyboardShortcutsService` calling a new synchronous, preload-local bridge method `adjustZoomLevel`, which steps the frame-bound temporary level through `webFrame.setZoomLevel` — never a main-process `webContents.setZoomLevel`, whose per-URL entry the app's `file://` path routing resets. Step and limits live in `libs/shared/interfaces` (`stepZoomLevel`: 0.5 per press like Electron's zoomIn/zoomOut roles, clamped to levels −4…6). On macOS the renderer sees the key before the application menu, and `preventDefault()` keeps the menu role from stepping a second time (Electron only performs the menu key equivalent in its unhandled-keyboard-event hook). Persistence is unchanged: the main process still reads the live level back on close, quit and reload. The zoom E2E now drives the real shortcuts (in, out, numpad, reset). Help dialog entries added and translated for all locales; contract updated in docs/architecture/workspace-shell.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
ab239b5043
commit
67327bfd29
38 files changed
+705
-31
No files matched your search
@@ -7,6 +7,7 @@ export * from './lib/dev-logger.util';
|
||||
export * from './lib/download-metadata.interface';
|
||||
export * from './lib/embedded-mpv-session.interface';
|
||||
export * from './lib/subtitle-style.util';
|
||||
export * from './lib/zoom-level.util';
|
||||
export * from './lib/electron-api.interface';
|
||||
export * from './lib/epg-channel-metadata.model';
|
||||
export * from './lib/epg-channel-with-programs.interface';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AppUpdateChannel } from './app-update-channel.util';
|
||||
import type { SourceProbeContext, SourceHealthResult } from './source-health';
|
||||
import type { XtreamConnectionFailure } from './xtream-connection-test';
|
||||
import type { ZoomLevelAction } from './zoom-level.util';
|
||||
import type {
|
||||
CatchupDownloadMetadata,
|
||||
DownloadRecoveryResult,
|
||||
@@ -718,6 +719,16 @@ export interface ElectronBridgeApi {
|
||||
*/
|
||||
toggleFullScreenWindow: () => Promise<ElectronBridgeWindowState>;
|
||||
closeWindow: () => Promise<void>;
|
||||
/**
|
||||
* Zoom shortcuts (Cmd/Ctrl and +/−/0). Synchronous and preload-local:
|
||||
* steps the FRAME-BOUND temporary zoom level through `webFrame` (never a
|
||||
* main-process `webContents.setZoomLevel`, whose per-URL entry the app's
|
||||
* `file://` path routing resets — issue #1109), clamped to
|
||||
* `ZOOM_LEVEL_MIN..ZOOM_LEVEL_MAX`, and returns the level now applied.
|
||||
* Persistence needs nothing from the caller: the main process reads the
|
||||
* live level back on close, quit and reload.
|
||||
*/
|
||||
adjustZoomLevel: (action: ZoomLevelAction) => number;
|
||||
getWindowState: () => Promise<ElectronBridgeWindowState>;
|
||||
onWindowStateChange: (
|
||||
callback: (state: ElectronBridgeWindowState) => void
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
clampZoomLevel,
|
||||
stepZoomLevel,
|
||||
ZOOM_LEVEL_MAX,
|
||||
ZOOM_LEVEL_MIN,
|
||||
ZOOM_LEVEL_STEP,
|
||||
} from './zoom-level.util';
|
||||
|
||||
describe('zoom level shortcuts contract', () => {
|
||||
it('steps by the Electron menu-role step in both directions', () => {
|
||||
expect(stepZoomLevel(0, 'in')).toBe(ZOOM_LEVEL_STEP);
|
||||
expect(stepZoomLevel(0, 'out')).toBe(-ZOOM_LEVEL_STEP);
|
||||
expect(stepZoomLevel(1, 'in')).toBe(1.5);
|
||||
expect(stepZoomLevel(-1.5, 'out')).toBe(-2);
|
||||
});
|
||||
|
||||
it('resets to the default level from anywhere', () => {
|
||||
expect(stepZoomLevel(3, 'reset')).toBe(0);
|
||||
expect(stepZoomLevel(-2.5, 'reset')).toBe(0);
|
||||
expect(stepZoomLevel(Number.NaN, 'reset')).toBe(0);
|
||||
});
|
||||
|
||||
it('clamps at the app limits, not at Chromium limits', () => {
|
||||
expect(stepZoomLevel(ZOOM_LEVEL_MAX, 'in')).toBe(ZOOM_LEVEL_MAX);
|
||||
expect(stepZoomLevel(ZOOM_LEVEL_MIN, 'out')).toBe(ZOOM_LEVEL_MIN);
|
||||
expect(stepZoomLevel(ZOOM_LEVEL_MAX + 3, 'in')).toBe(ZOOM_LEVEL_MAX);
|
||||
expect(stepZoomLevel(ZOOM_LEVEL_MAX + 3, 'out')).toBe(
|
||||
ZOOM_LEVEL_MAX - ZOOM_LEVEL_STEP
|
||||
);
|
||||
expect(clampZoomLevel(ZOOM_LEVEL_MIN - 1)).toBe(ZOOM_LEVEL_MIN);
|
||||
});
|
||||
|
||||
it('snaps an off-grid level to the next grid point in the requested direction', () => {
|
||||
// A level the macOS menu roles or an older store left between steps.
|
||||
expect(stepZoomLevel(0.3, 'in')).toBe(0.5);
|
||||
expect(stepZoomLevel(0.3, 'out')).toBe(0);
|
||||
expect(stepZoomLevel(-0.7, 'out')).toBe(-1);
|
||||
expect(stepZoomLevel(-0.7, 'in')).toBe(-0.5);
|
||||
});
|
||||
|
||||
it('treats a non-finite level as the default', () => {
|
||||
expect(stepZoomLevel(Number.NaN, 'in')).toBe(ZOOM_LEVEL_STEP);
|
||||
expect(stepZoomLevel(Number.POSITIVE_INFINITY, 'out')).toBe(
|
||||
-ZOOM_LEVEL_STEP
|
||||
);
|
||||
expect(clampZoomLevel(Number.NaN)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* App zoom shortcut contract (Cmd/Ctrl and +/−/0), shared by the renderer
|
||||
* key binding (`WorkspaceKeyboardShortcutsService`) and the preload, which
|
||||
* applies the result through `webFrame.setZoomLevel` (issue #1109: the level
|
||||
* must stay a frame-bound temporary zoom; see
|
||||
* `docs/architecture/workspace-shell.md`, "Zoom level").
|
||||
*
|
||||
* A Chromium zoom level is the exponent of a 1.2 factor: level 1 renders at
|
||||
* 120 %, level −1 at ~83 %. The step matches Electron's `zoomIn`/`zoomOut`
|
||||
* menu roles (0.5, about 10 % per press), so the level the macOS menu used
|
||||
* to produce and the level the shortcuts produce are the same series.
|
||||
*/
|
||||
|
||||
export type ZoomLevelAction = 'in' | 'out' | 'reset';
|
||||
|
||||
export const ZOOM_LEVEL_DEFAULT = 0;
|
||||
/** Electron's `zoomIn`/`zoomOut` role step. */
|
||||
export const ZOOM_LEVEL_STEP = 0.5;
|
||||
/** ≈48 % — below this the workspace chrome is unreadable. */
|
||||
export const ZOOM_LEVEL_MIN = -4;
|
||||
/** ≈299 % — well inside Chromium's own 25–500 % limits. */
|
||||
export const ZOOM_LEVEL_MAX = 6;
|
||||
|
||||
export function clampZoomLevel(level: number): number {
|
||||
if (!Number.isFinite(level)) {
|
||||
return ZOOM_LEVEL_DEFAULT;
|
||||
}
|
||||
|
||||
return Math.min(ZOOM_LEVEL_MAX, Math.max(ZOOM_LEVEL_MIN, level));
|
||||
}
|
||||
|
||||
/**
|
||||
* The level `action` produces from `current`. Steps are snapped to the step
|
||||
* grid so a level restored from an older store (or set by the macOS menu
|
||||
* before the shortcuts existed) does not carry a fractional offset forever.
|
||||
* A non-finite `current` is treated as the default.
|
||||
*/
|
||||
export function stepZoomLevel(
|
||||
current: number,
|
||||
action: ZoomLevelAction
|
||||
): number {
|
||||
if (action === 'reset') {
|
||||
return ZOOM_LEVEL_DEFAULT;
|
||||
}
|
||||
|
||||
const base = clampZoomLevel(current);
|
||||
const direction = action === 'in' ? 1 : -1;
|
||||
const steps = base / ZOOM_LEVEL_STEP;
|
||||
// A level between grid points steps to the next grid point in the
|
||||
// requested direction instead of skipping past it.
|
||||
const nextSteps = Number.isInteger(steps)
|
||||
? steps + direction
|
||||
: direction > 0
|
||||
? Math.ceil(steps)
|
||||
: Math.floor(steps);
|
||||
const next = nextSteps * ZOOM_LEVEL_STEP;
|
||||
|
||||
return clampZoomLevel(next);
|
||||
}
|
||||
Reference in new issue
Block a user