Browser pairing, notifications, element hiding; drop the top bar

Pairing decrypts a Chromium browser's cookie store (PBKDF2-HMAC-SHA1
against its Keychain key, then AES-128-CBC) and injects the result into
WKHTTPCookieStore. Only the configured apps' hosts and their sign-in
hosts survive the filter. Browsers are offered most-recently-used first,
since the first entry becomes the default and someone with four
Chromium browsers installed wants the one they actually browse in.

WKWebView defines window.Notification but it does nothing: constructing
one throws no error and shows no banner, so a page believes it notified
you. Measured on the machine as `api=function shim=no` before the shim
was made unconditional; `from page: Odoo - Test notification -> raised`
after.

Anything on a page can be right-clicked away. The rule is re-asserted on
every navigation, because the injected script only carries a snapshot
from when the view was built and a selector added since would otherwise
come back on reload.

The top bar is gone. Navigation lives beside the cog, the nav carries
the traffic lights, and two-finger swipe goes back and forward.

The seed is now the real app list, scoped to exact hosts so a Drive link
inside Gmail switches rather than being swallowed.
This commit is contained in:
2026-09-01 12:17:31 +02:00
parent e369c82774
commit ff4a0c6bc4
44 changed files with 2492 additions and 419 deletions
+40 -49
View File
@@ -2,18 +2,17 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react
import { listen } from "@tauri-apps/api/event";
import * as api from "./api";
import Nav, { navWidth } from "./components/Nav";
import Nav from "./components/Nav";
import Settings from "./components/Settings";
import TopBar from "./components/TopBar";
import { BTN_PRIMARY } from "./components/ui";
import { useAppearance, type Theme } from "./hooks/useAppearance";
import type { Config, Group, SwitchEvent, UrlEvent } from "./types";
import type { Config, Group, HiddenEvent, SwitchEvent } from "./types";
export default function App() {
const [config, setConfig] = useState<Config | null>(null);
const [activeId, setActiveId] = useState<string | null>(null);
const [url, setUrl] = useState<string | null>(null);
const [settingsOpen, setSettingsOpen] = useState(false);
const [focusHidden, setFocusHidden] = useState<string | null>(null);
const [theme, setTheme] = useAppearance("system");
const stageRef = useRef<HTMLDivElement>(null);
@@ -24,7 +23,6 @@ export default function App() {
activeRef.current = activeId;
const collapsed = config?.settings.navCollapsed ?? false;
const activeApp = config?.apps.find((a) => a.id === activeId) ?? null;
useEffect(() => {
api.getConfig().then((c) => {
@@ -72,20 +70,24 @@ export default function App() {
}, [config, report]);
useEffect(() => {
if (!activeId) return;
void api.setActive(activeId);
void api.currentUrl(activeId).then(setUrl);
if (activeId) void api.setActive(activeId);
}, [activeId]);
// A link in one app that points at another: Rust decided, the shell moves.
useEffect(() => {
const unlisten = [
// A link in one app that points at another: Rust decided, the shell moves.
listen<SwitchEvent>("switch-app", async (e) => {
setActiveId(e.payload.appId);
await api.navigateApp(e.payload.appId, e.payload.url);
}),
listen<UrlEvent>("url-changed", (e) => {
if (e.payload.appId === activeRef.current) setUrl(e.payload.url);
// Something was right-clicked away inside a page.
listen<HiddenEvent>("hidden-added", () => {
void api.getConfig().then(setConfig);
}),
// "Manage hidden elements…" from a page's context menu.
listen<string>("manage-hidden", (e) => {
setFocusHidden(e.payload);
setSettingsOpen(true);
}),
];
return () => {
@@ -110,10 +112,7 @@ export default function App() {
const toggleGroup = (g: Group) => {
if (!config) return;
const updated = { ...g, collapsed: !g.collapsed };
setConfig({
...config,
groups: config.groups.map((x) => (x.id === g.id ? updated : x)),
});
setConfig({ ...config, groups: config.groups.map((x) => (x.id === g.id ? updated : x)) });
void api.updateGroup(updated);
};
@@ -125,55 +124,47 @@ export default function App() {
if (!config) return null;
return (
<div className="flex h-screen flex-col">
<TopBar
url={url}
appName={activeApp?.name ?? null}
<div className="flex h-screen">
<Nav
config={config}
activeId={activeId}
collapsed={collapsed}
onSelect={setActiveId}
onToggleCollapse={toggleCollapse}
onOpenSettings={() => { setFocusHidden(null); setSettingsOpen(true); }}
onToggleGroup={toggleGroup}
onBack={() => activeId && api.historyGo(activeId, -1)}
onForward={() => activeId && api.historyGo(activeId, 1)}
onReload={() => activeId && api.historyGo(activeId, 0)}
onOpenExternal={() => url && api.openExternal(url)}
onPick={() => activeId && api.pickHidden(activeId)}
/>
<div className="flex min-h-0 flex-1">
<Nav
config={config}
activeId={activeId}
collapsed={collapsed}
onSelect={setActiveId}
onToggleCollapse={toggleCollapse}
onOpenSettings={() => setSettingsOpen(true)}
onToggleGroup={toggleGroup}
/>
{/* The hole an app's native webview is positioned into. It stays empty
on purpose — anything drawn here would be painted over. */}
<div ref={stageRef} className="min-w-0 flex-1 bg-slate-100 dark:bg-slate-950">
{config.apps.length === 0 && (
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
<p className="text-[13px] text-slate-500 dark:text-slate-400">
No apps yet. Add the tools you work in and they appear in the nav.
</p>
<button onClick={() => setSettingsOpen(true)} className={BTN_PRIMARY}>
Add your first app
</button>
</div>
)}
</div>
{/* The hole an app's native webview is positioned into. It stays empty
on purpose — anything drawn here would be painted over. */}
<div ref={stageRef} className="min-w-0 flex-1 bg-slate-100 dark:bg-slate-950">
{config.apps.length === 0 && (
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
<p className="text-[13px] text-slate-500 dark:text-slate-400">
No apps yet. Add the tools you work in and they appear in the nav.
</p>
<button onClick={() => setSettingsOpen(true)} className={BTN_PRIMARY}>
Add your first app
</button>
</div>
)}
</div>
{settingsOpen && (
<Settings
config={config}
theme={theme}
activeId={activeId}
focusHiddenFor={focusHidden}
onConfig={setConfig}
onTheme={onTheme}
onClose={() => setSettingsOpen(false)}
onClose={() => { setSettingsOpen(false); setFocusHidden(null); }}
/>
)}
{/* Keeps the nav width honest for the stage measurement above. */}
<span hidden>{navWidth(collapsed)}</span>
</div>
);
}