mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Add per-session zoom state, route the zoom factor through the desktop preview bridge, and apply it to the Electron WebContentsView before preview capture so screenshot and annotation coordinates stay on the native preview surface. Tested: - cd desktop && bun run test -- src/components/browser/BrowserSurface.test.tsx src/stores/browserPanelStore.test.ts src/components/workbench/WorkbenchPanel.webview.test.tsx src/lib/previewBridge.test.ts src/lib/desktopHost/electronHost.test.ts - cd desktop && bun test electron/services/preview.test.ts - cd desktop && bun run lint - cd desktop && bun run build - cd desktop && bun run check:electron Not-tested: - cd desktop && bun run check:desktop (blocked by existing MessageList relative-time assertion and TabBar AbortSignal errors) Confidence: medium Scope-risk: moderate
125 lines
4.9 KiB
TypeScript
125 lines
4.9 KiB
TypeScript
import { create } from 'zustand'
|
|
import { useWorkspacePanelStore } from './workspacePanelStore'
|
|
|
|
export const MIN_BROWSER_ZOOM = 0.5
|
|
export const MAX_BROWSER_ZOOM = 1.5
|
|
export const DEFAULT_BROWSER_ZOOM = 1
|
|
export const BROWSER_ZOOM_STEP = 0.1
|
|
|
|
export function normalizeBrowserZoom(value: unknown): number {
|
|
const numeric = typeof value === 'number' ? value : Number(value)
|
|
if (!Number.isFinite(numeric)) return DEFAULT_BROWSER_ZOOM
|
|
const clamped = Math.min(Math.max(numeric, MIN_BROWSER_ZOOM), MAX_BROWSER_ZOOM)
|
|
return Math.round(clamped * 10) / 10
|
|
}
|
|
|
|
export type BrowserSessionState = {
|
|
isOpen: boolean
|
|
url: string
|
|
title: string
|
|
history: string[]
|
|
historyIndex: number
|
|
loading: boolean
|
|
pickerActive: boolean
|
|
canGoBack: boolean
|
|
canGoForward: boolean
|
|
zoom: number
|
|
}
|
|
|
|
type BrowserPanelState = {
|
|
bySession: Record<string, BrowserSessionState>
|
|
open: (sessionId: string, url: string) => void
|
|
ensureBlank: (sessionId: string) => void
|
|
navigate: (sessionId: string, url: string) => void
|
|
goBack: (sessionId: string) => void
|
|
goForward: (sessionId: string) => void
|
|
setLoading: (sessionId: string, loading: boolean) => void
|
|
setPicker: (sessionId: string, active: boolean) => void
|
|
setZoom: (sessionId: string, zoom: number) => void
|
|
close: (sessionId: string) => void
|
|
setNavigated: (sessionId: string, url: string, title: string) => void
|
|
setReady: (sessionId: string) => void
|
|
}
|
|
|
|
const empty = (url = ''): BrowserSessionState => ({
|
|
isOpen: true,
|
|
url,
|
|
title: '',
|
|
history: url ? [url] : [],
|
|
historyIndex: url ? 0 : -1,
|
|
loading: false,
|
|
pickerActive: false,
|
|
canGoBack: false,
|
|
canGoForward: false,
|
|
zoom: DEFAULT_BROWSER_ZOOM,
|
|
})
|
|
|
|
const withNav = (s: BrowserSessionState): BrowserSessionState => ({
|
|
...s,
|
|
url: s.history[s.historyIndex] ?? s.url,
|
|
canGoBack: s.historyIndex > 0,
|
|
canGoForward: s.historyIndex < s.history.length - 1,
|
|
})
|
|
|
|
export const useBrowserPanelStore = create<BrowserPanelState>((set) => ({
|
|
bySession: {},
|
|
open: (sessionId, url) => {
|
|
set((st) => {
|
|
const zoom = st.bySession[sessionId]?.zoom ?? DEFAULT_BROWSER_ZOOM
|
|
return {
|
|
bySession: { ...st.bySession, [sessionId]: { ...empty(url), zoom, loading: true } },
|
|
}
|
|
})
|
|
// The browser is one mode of the unified workbench: opening a previewable
|
|
// link / localhost url surfaces the workbench in BROWSER mode. Import is
|
|
// one-directional (workspacePanelStore never imports this store), so no cycle.
|
|
const workspacePanel = useWorkspacePanelStore.getState()
|
|
workspacePanel.openPanel(sessionId)
|
|
workspacePanel.setMode(sessionId, 'browser')
|
|
},
|
|
ensureBlank: (sessionId) => set((st) => {
|
|
const cur = st.bySession[sessionId]
|
|
if (cur) {
|
|
return { bySession: { ...st.bySession, [sessionId]: { ...cur, isOpen: true } } }
|
|
}
|
|
return { bySession: { ...st.bySession, [sessionId]: empty() } }
|
|
}),
|
|
navigate: (sessionId, url) => set((st) => {
|
|
const cur = st.bySession[sessionId] ?? empty(url)
|
|
const history = [...cur.history.slice(0, Math.max(0, cur.historyIndex + 1)), url]
|
|
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, isOpen: true, loading: true, history, historyIndex: history.length - 1 }) } }
|
|
}),
|
|
goBack: (sessionId) => set((st) => {
|
|
const cur = st.bySession[sessionId]; if (!cur || cur.historyIndex <= 0) return st
|
|
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, historyIndex: cur.historyIndex - 1 }) } }
|
|
}),
|
|
goForward: (sessionId) => set((st) => {
|
|
const cur = st.bySession[sessionId]; if (!cur || cur.historyIndex >= cur.history.length - 1) return st
|
|
return { bySession: { ...st.bySession, [sessionId]: withNav({ ...cur, historyIndex: cur.historyIndex + 1 }) } }
|
|
}),
|
|
setLoading: (sessionId, loading) => set((st) => {
|
|
const cur = st.bySession[sessionId]; if (!cur) return st
|
|
return { bySession: { ...st.bySession, [sessionId]: { ...cur, loading } } }
|
|
}),
|
|
setPicker: (sessionId, active) => set((st) => {
|
|
const cur = st.bySession[sessionId]; if (!cur) return st
|
|
return { bySession: { ...st.bySession, [sessionId]: { ...cur, pickerActive: active } } }
|
|
}),
|
|
setZoom: (sessionId, zoom) => set((st) => {
|
|
const cur = st.bySession[sessionId]; if (!cur) return st
|
|
return { bySession: { ...st.bySession, [sessionId]: { ...cur, zoom: normalizeBrowserZoom(zoom) } } }
|
|
}),
|
|
close: (sessionId) => set((st) => {
|
|
const cur = st.bySession[sessionId]; if (!cur) return st
|
|
return { bySession: { ...st.bySession, [sessionId]: { ...cur, isOpen: false, pickerActive: false } } }
|
|
}),
|
|
setNavigated: (sessionId, url, title) => set((st) => {
|
|
const cur = st.bySession[sessionId]; if (!cur) return st
|
|
return { bySession: { ...st.bySession, [sessionId]: { ...cur, url, title, loading: false } } }
|
|
}),
|
|
setReady: (sessionId) => set((st) => {
|
|
const cur = st.bySession[sessionId]; if (!cur) return st
|
|
return { bySession: { ...st.bySession, [sessionId]: { ...cur, loading: false } } }
|
|
}),
|
|
}))
|