mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
feat(browser): per-site appearance and zoom, from the page menu and Cmd/Ctrl +/-/0 (#31621)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
browserPageZoomLevelToPercent,
|
||||
nextBrowserPageZoomLevel,
|
||||
normalizeBrowserPageZoomLevel
|
||||
} from './browser-page-zoom'
|
||||
|
||||
const percentsFrom = (level: number, direction: 'in' | 'out', steps: number): number[] => {
|
||||
const percents: number[] = []
|
||||
for (let step = 0; step < steps; step++) {
|
||||
level = nextBrowserPageZoomLevel(level, direction)
|
||||
percents.push(browserPageZoomLevelToPercent(level))
|
||||
}
|
||||
return percents
|
||||
}
|
||||
|
||||
describe('browser page zoom presets', () => {
|
||||
it("steps through Chrome's presets and stops at 25% and 500%", () => {
|
||||
expect(percentsFrom(0, 'in', 10)).toEqual([110, 125, 150, 175, 200, 250, 300, 400, 500, 500])
|
||||
expect(percentsFrom(0, 'out', 8)).toEqual([90, 80, 75, 67, 50, 33, 25, 25])
|
||||
})
|
||||
|
||||
it('snaps levels saved before the presets to the nearest one', () => {
|
||||
// 0.5-level steps used to give 110%, 131%, 144% and 173%.
|
||||
expect(
|
||||
[0.5, 1.5, 2, 3].map(normalizeBrowserPageZoomLevel).map(browserPageZoomLevelToPercent)
|
||||
).toEqual([110, 125, 150, 175])
|
||||
expect(normalizeBrowserPageZoomLevel(Number.NaN)).toBe(0)
|
||||
})
|
||||
|
||||
it('resets to the level it is given', () => {
|
||||
expect(nextBrowserPageZoomLevel(3, 'reset', normalizeBrowserPageZoomLevel(1.5))).toBe(
|
||||
normalizeBrowserPageZoomLevel(1.5)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,34 +1,49 @@
|
||||
export type BrowserPageZoomDirection = 'in' | 'out' | 'reset'
|
||||
|
||||
export function isBrowserPageZoomDirection(value: unknown): value is BrowserPageZoomDirection {
|
||||
return value === 'in' || value === 'out' || value === 'reset'
|
||||
}
|
||||
|
||||
export type BrowserPageZoomCommand = {
|
||||
browserPageId: string
|
||||
direction: BrowserPageZoomDirection
|
||||
}
|
||||
|
||||
export const BROWSER_PAGE_ZOOM_STEP = 0.5
|
||||
export const BROWSER_PAGE_ZOOM_MIN = -3
|
||||
export const BROWSER_PAGE_ZOOM_MAX = 5
|
||||
export const DEFAULT_BROWSER_PAGE_ZOOM_LEVEL = 0
|
||||
|
||||
export const BROWSER_PAGE_ZOOM_LEVELS: readonly number[] = Array.from(
|
||||
{
|
||||
length: Math.round((BROWSER_PAGE_ZOOM_MAX - BROWSER_PAGE_ZOOM_MIN) / BROWSER_PAGE_ZOOM_STEP) + 1
|
||||
},
|
||||
(_, index) => BROWSER_PAGE_ZOOM_MIN + index * BROWSER_PAGE_ZOOM_STEP
|
||||
// Why: Chrome's page zoom presets; Electron levels are log base 1.2 of the zoom factor.
|
||||
const BROWSER_PAGE_ZOOM_PERCENTS = [
|
||||
25, 33, 50, 67, 75, 80, 90, 100, 110, 125, 150, 175, 200, 250, 300, 400, 500
|
||||
] as const
|
||||
|
||||
export const BROWSER_PAGE_ZOOM_LEVELS: readonly number[] = BROWSER_PAGE_ZOOM_PERCENTS.map(
|
||||
(percent) => Math.log(percent / 100) / Math.log(1.2)
|
||||
)
|
||||
|
||||
export function normalizeBrowserPageZoomLevel(value: unknown): number {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||
return DEFAULT_BROWSER_PAGE_ZOOM_LEVEL
|
||||
/** The nearest preset; anything that is not a finite level counts as 100%. */
|
||||
function nearestZoomLevelIndex(level: unknown): number {
|
||||
if (typeof level !== 'number' || !Number.isFinite(level)) {
|
||||
return BROWSER_PAGE_ZOOM_PERCENTS.indexOf(100)
|
||||
}
|
||||
const roundedToStep = Math.round(value / BROWSER_PAGE_ZOOM_STEP) * BROWSER_PAGE_ZOOM_STEP
|
||||
return Math.max(BROWSER_PAGE_ZOOM_MIN, Math.min(BROWSER_PAGE_ZOOM_MAX, roundedToStep))
|
||||
let nearest = 0
|
||||
for (let index = 1; index < BROWSER_PAGE_ZOOM_LEVELS.length; index++) {
|
||||
if (
|
||||
Math.abs(BROWSER_PAGE_ZOOM_LEVELS[index] - level) <
|
||||
Math.abs(BROWSER_PAGE_ZOOM_LEVELS[nearest] - level)
|
||||
) {
|
||||
nearest = index
|
||||
}
|
||||
}
|
||||
return nearest
|
||||
}
|
||||
|
||||
/** Snaps any level, including ones saved before the presets, to the nearest preset. */
|
||||
export function normalizeBrowserPageZoomLevel(value: unknown): number {
|
||||
return BROWSER_PAGE_ZOOM_LEVELS[nearestZoomLevelIndex(value)]
|
||||
}
|
||||
|
||||
export function browserPageZoomLevelToPercent(level: number): number {
|
||||
// Why: Electron zoom levels are exponential; show the same percentage users
|
||||
// expect from Chromium browser zoom controls.
|
||||
return Math.round(100 * 1.2 ** normalizeBrowserPageZoomLevel(level))
|
||||
return BROWSER_PAGE_ZOOM_PERCENTS[nearestZoomLevelIndex(level)]
|
||||
}
|
||||
|
||||
export function nextBrowserPageZoomLevel(
|
||||
@@ -36,12 +51,10 @@ export function nextBrowserPageZoomLevel(
|
||||
direction: BrowserPageZoomDirection,
|
||||
resetLevel: number = DEFAULT_BROWSER_PAGE_ZOOM_LEVEL
|
||||
): number {
|
||||
const rawNext =
|
||||
direction === 'in'
|
||||
? current + BROWSER_PAGE_ZOOM_STEP
|
||||
: direction === 'out'
|
||||
? current - BROWSER_PAGE_ZOOM_STEP
|
||||
: normalizeBrowserPageZoomLevel(resetLevel)
|
||||
|
||||
return normalizeBrowserPageZoomLevel(rawNext)
|
||||
if (direction === 'reset') {
|
||||
return normalizeBrowserPageZoomLevel(resetLevel)
|
||||
}
|
||||
const index = nearestZoomLevelIndex(current)
|
||||
const next = direction === 'in' ? index + 1 : index - 1
|
||||
return BROWSER_PAGE_ZOOM_LEVELS[Math.max(0, Math.min(BROWSER_PAGE_ZOOM_LEVELS.length - 1, next))]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { BROWSER_SITE_PERMISSION_LOCAL_FILES_ORIGIN } from './browser-site-permission'
|
||||
|
||||
/** A site's own color scheme; a site with none matches Orca's theme. */
|
||||
export type BrowserSiteColorScheme = 'light' | 'dark'
|
||||
|
||||
/** What a browser profile remembers about how one site is shown. */
|
||||
export type BrowserSiteDisplay = {
|
||||
zoomLevel?: number
|
||||
colorScheme?: BrowserSiteColorScheme
|
||||
}
|
||||
|
||||
/** How the page in a tab is shown right now, for the browser menu. */
|
||||
export type BrowserPageSiteDisplay = {
|
||||
/** Null for pages no site owns; neither setting applies to them. */
|
||||
site: string | null
|
||||
zoomLevel: number
|
||||
/** False while the site uses Default Zoom. */
|
||||
siteHasZoom: boolean
|
||||
colorScheme: BrowserSiteColorScheme | null
|
||||
}
|
||||
|
||||
export function isBrowserSiteColorScheme(value: unknown): value is BrowserSiteColorScheme {
|
||||
return value === 'light' || value === 'dark'
|
||||
}
|
||||
|
||||
/**
|
||||
* The hostname, port ignored: Chromium shares page zoom across ports, so `localhost:3000` and
|
||||
* `localhost:5173` must be one site or they would keep resetting each other. Null for pages no site
|
||||
* owns (about:blank, data:, chrome errors).
|
||||
*/
|
||||
export function browserSiteDisplayKey(url: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
if (parsed.protocol === 'file:') {
|
||||
return BROWSER_SITE_PERMISSION_LOCAL_FILES_ORIGIN
|
||||
}
|
||||
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
||||
return parsed.hostname || null
|
||||
}
|
||||
} catch {
|
||||
// Not a URL; no site.
|
||||
}
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user