mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] browser: zoom focused browser + Ctrl+F find + Ctrl+Tab cycle + middle-click bg tab
This commit is contained in:
+26
-1
@@ -1990,6 +1990,27 @@ function routeReloadShortcut(event, input) {
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// In-page browser shortcuts (zoom, find, tab-cycle) for a focused <webview> guest. Keydowns inside a
|
||||
// guest never reach the host renderer, so we catch them here and forward the intent + the guest's
|
||||
// webContents id so the renderer can target that exact browser. Attached to guests ONLY: on the host
|
||||
// the renderer's own keydown handles canvas-vs-browser, and intercepting there would eat canvas zoom.
|
||||
function routeBrowserShortcut(event, input, webContentsId) {
|
||||
if (input.type !== 'keyDown' || input.alt) return;
|
||||
const mod = input.meta || input.control;
|
||||
const key = (input.key || '').toLowerCase();
|
||||
let action = null;
|
||||
if (mod && !input.shift && (key === '=' || key === '+')) action = 'zoom-in';
|
||||
else if (mod && !input.shift && key === '-') action = 'zoom-out';
|
||||
else if (mod && !input.shift && key === '0') action = 'zoom-reset';
|
||||
else if (mod && !input.shift && key === 'f') action = 'find';
|
||||
else if (input.control && !input.meta && key === 'tab') action = input.shift ? 'tab-prev' : 'tab-next';
|
||||
if (!action) return;
|
||||
event.preventDefault();
|
||||
try {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('openswarm:browser-shortcut', { action, webContentsId });
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
app.on('web-contents-created', (_event, contents) => {
|
||||
// Block Cmd+W from closing the main window, whether the window chrome or one of
|
||||
// its embedded webviews has focus. OAuth popups (their own 'window' contents,
|
||||
@@ -1999,6 +2020,10 @@ app.on('web-contents-created', (_event, contents) => {
|
||||
contents.on('before-input-event', swallowCloseWindowShortcut);
|
||||
contents.on('before-input-event', routeReloadShortcut);
|
||||
}
|
||||
if (contents.getType() === 'webview') {
|
||||
const wcId = contents.id;
|
||||
contents.on('before-input-event', (event, input) => routeBrowserShortcut(event, input, wcId));
|
||||
}
|
||||
|
||||
// Override the user-agent on popup BrowserWindows (i.e. anything created
|
||||
// via window.open from the renderer, which includes the OAuth popup for
|
||||
@@ -2033,7 +2058,7 @@ app.on('web-contents-created', (_event, contents) => {
|
||||
contents.setWindowOpenHandler(({ url, disposition }) => {
|
||||
if (disposition === 'foreground-tab' || disposition === 'background-tab') {
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send('webview-new-window', url, contents.id);
|
||||
mainWindow.webContents.send('webview-new-window', url, contents.id, disposition);
|
||||
}
|
||||
return { action: 'deny' };
|
||||
}
|
||||
|
||||
+8
-1
@@ -103,7 +103,7 @@ contextBridge.exposeInMainWorld('openswarm', {
|
||||
},
|
||||
|
||||
onWebviewNewWindow: (cb) => {
|
||||
const listener = (_event, url, webContentsId) => cb(url, webContentsId);
|
||||
const listener = (_event, url, webContentsId, disposition) => cb(url, webContentsId, disposition);
|
||||
ipcRenderer.on('webview-new-window', listener);
|
||||
return () => ipcRenderer.removeListener('webview-new-window', listener);
|
||||
},
|
||||
@@ -115,6 +115,13 @@ contextBridge.exposeInMainWorld('openswarm', {
|
||||
return () => ipcRenderer.removeListener('openswarm:reload-shortcut', listener);
|
||||
},
|
||||
|
||||
// In-page browser shortcuts (zoom/find/tab-cycle) from a focused guest webview, carrying the guest's webContents id so the renderer targets that exact browser.
|
||||
onBrowserShortcut: (cb) => {
|
||||
const listener = (_event, payload) => cb(payload);
|
||||
ipcRenderer.on('openswarm:browser-shortcut', listener);
|
||||
return () => ipcRenderer.removeListener('openswarm:browser-shortcut', listener);
|
||||
},
|
||||
|
||||
// Deep-link callback: fires when the OS opens the app with an
|
||||
// openswarm://auth?token=... URL (after Stripe-hosted checkout).
|
||||
onAuthUrl: (cb) => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { openSettingsModal } from '@/shared/state/settingsSlice';
|
||||
import { getLastInteractedBrowser, getKeepAliveBrowserIds, setLastInteractedBrowser, clearLastInteractedBrowser } from '@/shared/browserFocus';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { applyBrowserZoom } from '@/shared/browserZoom';
|
||||
import Box from '@mui/material/Box';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
@@ -42,7 +43,7 @@ import { shallowEqual } from 'react-redux';
|
||||
import { fetchDashboards, createDashboard, renameDashboard } from '@/shared/state/dashboardsSlice';
|
||||
import { Typewriter } from '@/app/components/feedback/Animated';
|
||||
import { setPendingFocusAgentId } from '@/shared/state/tempStateSlice';
|
||||
import { addBrowserCard, addBrowserTab } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { addBrowserCard, addBrowserTab, cycleBrowserTab } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { setPendingBrowserUrl } from '@/shared/state/tempStateSlice';
|
||||
import { fetchOutputs } from '@/shared/state/outputsSlice';
|
||||
import { setInstalling } from '@/shared/state/updateSlice';
|
||||
@@ -256,13 +257,14 @@ const AppShell: React.FC = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openUrlInBrowser = useCallback((url: string, webContentsId?: number) => {
|
||||
const openUrlInBrowser = useCallback((url: string, webContentsId?: number, background?: boolean) => {
|
||||
const dashMatch = location.pathname.match(/^\/dashboard\/(.+)/);
|
||||
if (dashMatch) {
|
||||
if (webContentsId != null) {
|
||||
const browserId = findBrowserByWebContentsId(webContentsId);
|
||||
if (browserId) {
|
||||
dispatch(addBrowserTab({ browserId, url, makeActive: true }));
|
||||
// Middle-click / background-tab disposition: add the tab but don't steal focus from the current one, like a real browser.
|
||||
dispatch(addBrowserTab({ browserId, url, makeActive: !background }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -316,12 +318,12 @@ const AppShell: React.FC = () => {
|
||||
if (!w.openswarm?.onWebviewNewWindow) return;
|
||||
let lastUrl = '';
|
||||
let lastTime = 0;
|
||||
return w.openswarm.onWebviewNewWindow((url: string, webContentsId: number) => {
|
||||
return w.openswarm.onWebviewNewWindow((url: string, webContentsId: number, disposition?: string) => {
|
||||
const now = Date.now();
|
||||
if (url === lastUrl && now - lastTime < 1000) return;
|
||||
lastUrl = url;
|
||||
lastTime = now;
|
||||
openUrlInBrowser(url, webContentsId);
|
||||
openUrlInBrowser(url, webContentsId, disposition === 'background-tab');
|
||||
});
|
||||
}, [openUrlInBrowser]);
|
||||
|
||||
@@ -349,6 +351,43 @@ const AppShell: React.FC = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Zoom / find / tab-cycle from a focused browser GUEST (keydowns inside a webview can't reach this document, so main forwards them with the guest's id). Targets that exact browser; the host-focused counterparts live in the keydown below + useCanvasControls (zoom).
|
||||
useEffect(() => {
|
||||
const w = window as any;
|
||||
if (!w.openswarm?.onBrowserShortcut) return;
|
||||
return w.openswarm.onBrowserShortcut((payload: { action: string; webContentsId: number }) => {
|
||||
const id = findBrowserByWebContentsId(payload.webContentsId) ?? getLastInteractedBrowser();
|
||||
if (!id) return;
|
||||
switch (payload.action) {
|
||||
case 'zoom-in': applyBrowserZoom(id, 1); break;
|
||||
case 'zoom-out': applyBrowserZoom(id, -1); break;
|
||||
case 'zoom-reset': applyBrowserZoom(id, 0); break;
|
||||
case 'find': window.dispatchEvent(new CustomEvent('openswarm:browser-find', { detail: { browserId: id } })); break;
|
||||
case 'tab-next': dispatch(cycleBrowserTab({ browserId: id, dir: 1 })); break;
|
||||
case 'tab-prev': dispatch(cycleBrowserTab({ browserId: id, dir: -1 })); break;
|
||||
}
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
// Host-focused Ctrl/Cmd+F (find) and Ctrl+Tab (cycle) when a browser is the last thing you touched. Zoom keys aren't here: they share the +/-/0 keys with canvas zoom, so useCanvasControls owns that branch.
|
||||
useEffect(() => {
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
const id = getLastInteractedBrowser();
|
||||
if (!id) return;
|
||||
const t = e.target as HTMLElement | null;
|
||||
const typing = t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || !!t?.isContentEditable;
|
||||
if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && (e.key || '').toLowerCase() === 'f' && !typing) {
|
||||
e.preventDefault();
|
||||
window.dispatchEvent(new CustomEvent('openswarm:browser-find', { detail: { browserId: id } }));
|
||||
} else if (e.ctrlKey && !e.metaKey && !e.altKey && e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
dispatch(cycleBrowserTab({ browserId: id, dir: e.shiftKey ? -1 : 1 }));
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
try { localStorage.setItem(SIDEBAR_WIDTH_KEY, String(sidebarWidth)); } catch {}
|
||||
}, [sidebarWidth]);
|
||||
|
||||
@@ -47,6 +47,7 @@ import {
|
||||
type BrowserWebview,
|
||||
} from '@/shared/browserRegistry';
|
||||
import { setLastInteractedBrowser } from '@/shared/browserFocus';
|
||||
import BrowserFindBar from './BrowserFindBar';
|
||||
import { useBrowserActivity } from '@/shared/useBrowserActivity';
|
||||
import { getActionLabel } from '@/shared/browserCommandHandler';
|
||||
import { resolveInput, isGoogleSearch } from '@/shared/resolveUrl';
|
||||
@@ -216,6 +217,9 @@ const BrowserCard: React.FC<Props> = ({
|
||||
// Electron webviews can't trigger OS platform auth; preload sends "passkey-detected" and we explain via modal.
|
||||
const [passkeyDialogOpen, setPasskeyDialogOpen] = useState(false);
|
||||
const [crashedTabs, setCrashedTabs] = useState<Set<string>>(new Set());
|
||||
// Ctrl/Cmd+F find bar; focusSignal re-focuses the input each time Ctrl+F fires while it's already open.
|
||||
const [findOpen, setFindOpen] = useState(false);
|
||||
const [findFocusSignal, setFindFocusSignal] = useState(0);
|
||||
const updateTabLocal = useCallback((tabId: string, update: Partial<TabLocalState>) => {
|
||||
setTabLocalStates((prev) => {
|
||||
const existing = prev[tabId] ?? { loading: false, canGoBack: false, canGoForward: false };
|
||||
@@ -244,6 +248,17 @@ const BrowserCard: React.FC<Props> = ({
|
||||
setRegistryActiveTab(browserId, activeTabId);
|
||||
}, [browserId, activeTabId]);
|
||||
|
||||
// Open the find bar when AppShell routes a Ctrl/Cmd+F to this browser; re-trigger re-focuses the input.
|
||||
useEffect(() => {
|
||||
const onFind = (e: Event) => {
|
||||
if ((e as CustomEvent).detail?.browserId !== browserId) return;
|
||||
setFindOpen(true);
|
||||
setFindFocusSignal((n) => n + 1);
|
||||
};
|
||||
window.addEventListener('openswarm:browser-find', onFind as EventListener);
|
||||
return () => window.removeEventListener('openswarm:browser-find', onFind as EventListener);
|
||||
}, [browserId]);
|
||||
|
||||
// A resumed webview remounts at about:blank; dropping the init markers lets doLoad re-fire.
|
||||
useEffect(() => {
|
||||
if (suspendedSnap) initializedTabs.current.clear();
|
||||
@@ -1097,6 +1112,9 @@ const BrowserCard: React.FC<Props> = ({
|
||||
|
||||
{/* Browser body: stacked webviews */}
|
||||
<Box sx={{ flex: 1, position: 'relative', overflow: 'hidden' }}>
|
||||
{findOpen && !suspendedSnap && (
|
||||
<BrowserFindBar browserId={browserId} focusSignal={findFocusSignal} onClose={() => setFindOpen(false)} />
|
||||
)}
|
||||
{isElementSelectMode && (
|
||||
<Box sx={{ position: 'absolute', inset: 0, zIndex: 10, pointerEvents: 'none' }} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import InputBase from '@mui/material/InputBase';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
|
||||
|
||||
interface BrowserFindBarProps {
|
||||
browserId: string;
|
||||
focusSignal: number;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// In-page find (Ctrl/Cmd+F): drives the active tab's webview findInPage with a Chrome-style match counter + up/down/Enter nav, clearing the highlight on close.
|
||||
export default function BrowserFindBar({ browserId, focusSignal, onClose }: BrowserFindBarProps): React.ReactElement {
|
||||
const c = useClaudeTokens();
|
||||
const [query, setQuery] = useState('');
|
||||
const [result, setResult] = useState<{ active: number; total: number }>({ active: 0, total: 0 });
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Fresh search omits findNext (passing findNext:false to a webview eats the found-in-page result, an Electron quirk); navigate=true does next/prev.
|
||||
const search = useCallback((text: string, navigate: boolean, forward: boolean) => {
|
||||
const wv = getWebview(browserId);
|
||||
if (!wv) return;
|
||||
try {
|
||||
if (!text) {
|
||||
wv.stopFindInPage('clearSelection');
|
||||
setResult({ active: 0, total: 0 });
|
||||
return;
|
||||
}
|
||||
if (navigate) wv.findInPage(text, { findNext: true, forward });
|
||||
else wv.findInPage(text);
|
||||
} catch {
|
||||
// torn-down webview; nothing to find
|
||||
}
|
||||
}, [browserId]);
|
||||
|
||||
useEffect(() => {
|
||||
const wv = getWebview(browserId);
|
||||
if (!wv) return;
|
||||
const onFound = (e: any) => {
|
||||
const r = e?.result;
|
||||
if (r && typeof r.matches === 'number') {
|
||||
setResult({ active: r.activeMatchOrdinal ?? 0, total: r.matches });
|
||||
}
|
||||
};
|
||||
wv.addEventListener('found-in-page', onFound as any);
|
||||
return () => {
|
||||
try {
|
||||
wv.removeEventListener('found-in-page', onFound as any);
|
||||
wv.stopFindInPage('clearSelection');
|
||||
} catch {
|
||||
// webview already gone
|
||||
}
|
||||
};
|
||||
}, [browserId]);
|
||||
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
}, [focusSignal]);
|
||||
|
||||
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const text = e.target.value;
|
||||
setQuery(text);
|
||||
search(text, false, true);
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if (query) search(query, true, !e.shiftKey);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 12,
|
||||
zIndex: 30,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
px: 1,
|
||||
py: 0.5,
|
||||
bgcolor: c.bg.surface,
|
||||
border: `1px solid ${c.border.medium}`,
|
||||
borderRadius: `${c.radius.md}px`,
|
||||
boxShadow: c.shadow.lg,
|
||||
}}
|
||||
>
|
||||
<InputBase
|
||||
inputRef={inputRef}
|
||||
value={query}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder="Find in page"
|
||||
sx={{ fontSize: 13, color: c.text.primary, width: 160, '& input': { p: 0 } }}
|
||||
/>
|
||||
<Box sx={{ fontSize: 12, color: c.text.tertiary, minWidth: 44, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
|
||||
{query ? `${result.active}/${result.total}` : ''}
|
||||
</Box>
|
||||
<IconButton size="small" disabled={!query} onClick={() => search(query, true, false)} sx={{ color: c.text.secondary }}>
|
||||
<KeyboardArrowUpIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
<IconButton size="small" disabled={!query} onClick={() => search(query, true, true)} sx={{ color: c.text.secondary }}>
|
||||
<KeyboardArrowDownIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
<IconButton size="small" onClick={onClose} sx={{ color: c.text.secondary }}>
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useState, useCallback, useRef, useEffect, useMemo, RefObject } from 'react';
|
||||
import { setCanvasInteractionActive } from '@/shared/canvasInteractionState';
|
||||
import { getLastInteractedBrowser } from '@/shared/browserFocus';
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
import { applyBrowserZoom } from '@/shared/browserZoom';
|
||||
|
||||
const MIN_ZOOM = 0.15;
|
||||
const MAX_ZOOM = 3.0;
|
||||
@@ -497,15 +500,21 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?:
|
||||
setCmdHeld(true);
|
||||
}
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
// If the last thing you touched was a browser card, +/-/0 zooms THAT page (like a real browser); otherwise it zooms the dashboard canvas.
|
||||
const focusedBrowser = getLastInteractedBrowser();
|
||||
const browserWv = focusedBrowser ? getWebview(focusedBrowser) : undefined;
|
||||
if (e.key === '0') {
|
||||
e.preventDefault();
|
||||
resetZoomRef.current();
|
||||
if (browserWv) applyBrowserZoom(focusedBrowser as string, 0);
|
||||
else resetZoomRef.current();
|
||||
} else if (e.key === '=' || e.key === '+') {
|
||||
e.preventDefault();
|
||||
zoomInRef.current();
|
||||
if (browserWv) applyBrowserZoom(focusedBrowser as string, 1);
|
||||
else zoomInRef.current();
|
||||
} else if (e.key === '-') {
|
||||
e.preventDefault();
|
||||
zoomOutRef.current();
|
||||
if (browserWv) applyBrowserZoom(focusedBrowser as string, -1);
|
||||
else zoomOutRef.current();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { removeNote, removeWorkflowCard, closeWorkflowsHub } from '@/shared/stat
|
||||
import { closeWorkflowCard } from '@/shared/state/workflowsSlice';
|
||||
import { removeBrowserCardCleanly } from '@/shared/browserTeardown';
|
||||
import { removeViewCardCleanly } from '@/shared/viewTeardown';
|
||||
import { getLastInteractedBrowser } from '@/shared/browserFocus';
|
||||
import type { useDashboardSelection } from '../state/useDashboardSelection';
|
||||
|
||||
type Selection = ReturnType<typeof useDashboardSelection>;
|
||||
@@ -117,6 +118,8 @@ export function useDashboardShortcuts({
|
||||
const handleSearch = (e: KeyboardEvent) => {
|
||||
if (!isActive) return; // Don't fire shortcuts when dashboard is hidden
|
||||
if (!(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== 'f') return;
|
||||
// When you're in a browser card, Cmd+F is find-in-page (handled in AppShell), not card search.
|
||||
if (getLastInteractedBrowser()) return;
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return;
|
||||
e.preventDefault();
|
||||
|
||||
@@ -27,6 +27,10 @@ export interface BrowserWebview extends HTMLElement {
|
||||
executeJavaScript: (code: string) => Promise<any>;
|
||||
sendInputEvent: (event: any) => void;
|
||||
getWebContentsId: () => number;
|
||||
getZoomLevel: () => number;
|
||||
setZoomLevel: (level: number) => void;
|
||||
findInPage: (text: string, options?: { forward?: boolean; findNext?: boolean; matchCase?: boolean }) => number;
|
||||
stopFindInPage: (action: 'clearSelection' | 'keepSelection' | 'activateSelection') => void;
|
||||
addEventListener: (event: string, listener: (...args: any[]) => void, options?: boolean | AddEventListenerOptions) => void;
|
||||
removeEventListener: (event: string, listener: (...args: any[]) => void, options?: boolean | EventListenerOptions) => void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { getWebview } from '@/shared/browserRegistry';
|
||||
|
||||
// Zoom the active-tab page of a browser card like a real browser (Ctrl/Cmd +/-/0), independent of the
|
||||
// dashboard canvas zoom. dir: 1 = in, -1 = out, 0 = reset. Step + clamp must match wherever this is
|
||||
// called so guest-focused and host-focused zoom never drift apart. Clamp ~0.5x..2.5x.
|
||||
const ZOOM_STEP = 0.5;
|
||||
const ZOOM_MIN = -3;
|
||||
const ZOOM_MAX = 5;
|
||||
|
||||
export function applyBrowserZoom(browserId: string, dir: -1 | 0 | 1): void {
|
||||
const wv = getWebview(browserId);
|
||||
if (!wv) return;
|
||||
try {
|
||||
if (dir === 0) {
|
||||
wv.setZoomLevel(0);
|
||||
return;
|
||||
}
|
||||
const raw = typeof wv.getZoomLevel === 'function' ? wv.getZoomLevel() : 0;
|
||||
const current = typeof raw === 'number' && isFinite(raw) ? raw : 0;
|
||||
const next = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, current + dir * ZOOM_STEP));
|
||||
wv.setZoomLevel(next);
|
||||
} catch {
|
||||
// torn-down webview; nothing to zoom
|
||||
}
|
||||
}
|
||||
@@ -1094,6 +1094,21 @@ const dashboardLayoutSlice = createSlice({
|
||||
}
|
||||
},
|
||||
|
||||
// Ctrl+Tab / Ctrl+Shift+Tab: move to the next/previous tab, wrapping around. dir 1 = forward.
|
||||
cycleBrowserTab(
|
||||
state,
|
||||
action: PayloadAction<{ browserId: string; dir: 1 | -1 }>
|
||||
) {
|
||||
const card = state.browserCards[action.payload.browserId];
|
||||
if (!card || card.tabs.length < 2) return;
|
||||
const idx = card.tabs.findIndex((t) => t.id === card.activeTabId);
|
||||
if (idx === -1) return;
|
||||
const n = card.tabs.length;
|
||||
const next = card.tabs[(idx + action.payload.dir + n) % n];
|
||||
card.activeTabId = next.id;
|
||||
card.url = next.url;
|
||||
},
|
||||
|
||||
updateBrowserTabUrl(
|
||||
state,
|
||||
action: PayloadAction<{ browserId: string; tabId: string; url: string }>
|
||||
@@ -1454,6 +1469,7 @@ export const {
|
||||
addBrowserTab,
|
||||
removeBrowserTab,
|
||||
setActiveBrowserTab,
|
||||
cycleBrowserTab,
|
||||
updateBrowserTabUrl,
|
||||
updateBrowserTabTitle,
|
||||
updateBrowserTabFavicon,
|
||||
|
||||
Vendored
+2
-1
@@ -46,8 +46,9 @@ declare global {
|
||||
onDownloadProgress: (cb: (progress: OpenSwarmDownloadProgress) => void) => () => void;
|
||||
onUpdateDownloaded: (cb: (info: OpenSwarmUpdateInfo) => void) => () => void;
|
||||
onUpdateError: (cb: (message: string) => void) => () => void;
|
||||
onWebviewNewWindow: (cb: (url: string, webContentsId: number) => void) => () => void;
|
||||
onWebviewNewWindow: (cb: (url: string, webContentsId: number, disposition?: string) => void) => () => void;
|
||||
onReloadShortcut?: (cb: () => void) => () => void;
|
||||
onBrowserShortcut?: (cb: (payload: { action: string; webContentsId: number }) => void) => () => void;
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
hardReset?: () => Promise<void>;
|
||||
clearBrowserData?: () => Promise<{ ok: boolean }>;
|
||||
|
||||
Reference in New Issue
Block a user