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:
+40
-49
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user