Drop cookie import; click-through notifications; per-app zoom

The cookie import is gone. It worked mechanically - 43 cookies decrypted
from Arc and verifiably visible to the page - but Google, Microsoft and
Odoo all refused the imported sessions, because each binds a session to
the browser that created it. Signing in once inside the app is simpler
and actually works, so the whole path is deleted rather than kept as a
feature that mostly fails. That takes rusqlite, aes, cbc, pbkdf2, hmac,
sha1 and sha2 out of the build with it.

Notifications are now raised through mac-notification-sys rather than
Tauri's notification plugin, because the plugin cannot report that one
was clicked. A click switches to the app that raised it and then runs
the page's own click handler - the only thing that knows which message
the notification was about.

Zoom is per app, on a fixed ladder so Cmd+0 returns to exactly 100%.
The shortcuts are menu-bar accelerators rather than a key listener,
since the keystroke has to work while a remote page has focus.

The hidden-element count is off the nav rows.
This commit is contained in:
2026-09-01 12:53:52 +02:00
parent f057268103
commit 3525d454bf
18 changed files with 416 additions and 1073 deletions
+32 -94
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import * as api from "../api";
import type { Theme } from "../hooks/useAppearance";
import type { Browser, Config, Group, PairResult, WorkApp } from "../types";
import type { Config, Group, WorkApp } from "../types";
import { Trash } from "./icons";
import {
BTN,
@@ -17,7 +17,6 @@ import {
SUBPANEL,
SectionHeading,
Segmented,
Spinner,
} from "./ui";
interface Props {
@@ -42,12 +41,7 @@ export default function Settings({
const [error, setError] = useState<string | null>(null);
const [confirmDelete, setConfirmDelete] = useState<WorkApp | null>(null);
const [browsers, setBrowsers] = useState<Browser[]>([]);
const [browser, setBrowser] = useState("");
const [pairing, setPairing] = useState(false);
const [notifyStatus, setNotifyStatus] = useState<string | null>(null);
const [cookieStatus, setCookieStatus] = useState<string | null>(null);
const [paired, setPaired] = useState<PairResult | null>(null);
const groups = [...config.groups].sort((a, b) => a.order - b.order);
@@ -58,12 +52,6 @@ export default function Settings({
(a, b) => groupRank(a.groupId) - groupRank(b.groupId) || a.order - b.order,
);
useEffect(() => {
api.listBrowsers().then((b) => {
setBrowsers(b);
setBrowser(config.settings.pairedBrowser ?? b[0]?.id ?? "");
});
}, [config.settings.pairedBrowser]);
useEffect(() => {
if (!focusHiddenFor) return;
@@ -95,20 +83,6 @@ export default function Settings({
const patch = (app: WorkApp, fields: Partial<WorkApp>) =>
run(() => api.updateApp({ ...app, ...fields }));
const pair = async () => {
if (!browser) return;
setPairing(true);
setPaired(null);
try {
setPaired(await api.pairBrowser(browser));
onConfig(await api.getConfig());
setError(null);
} catch (e) {
setError(String(e));
} finally {
setPairing(false);
}
};
const withHidden = orderedApps.filter((a) => a.hidden.length > 0);
@@ -247,75 +221,38 @@ export default function Settings({
</p>
</section>
{/* -------------------------------------------------- pairing */}
{/* ------------------------------------------------------ zoom */}
<section className="space-y-2">
<SectionHeading>Browser pairing</SectionHeading>
{browsers.length === 0 ? (
<p className={HELP}>No browser with a readable cookie store was found.</p>
) : (
<>
<div className={`${SUBPANEL} flex items-end gap-2`}>
<label className="flex-1 space-y-1">
<span className={LABEL}>Import cookies from</span>
<select value={browser} onChange={(e) => setBrowser(e.target.value)}
className={`${INPUT} w-full cursor-pointer`}>
{browsers.map((b) => (
<option key={b.id} value={b.id}>{b.label}</option>
))}
</select>
</label>
<button
onClick={async () => {
if (!activeId) return;
setCookieStatus("checking…");
await api.probeCookies(activeId);
await new Promise((r) => setTimeout(r, 400));
setCookieStatus(await api.cookieProbe());
}}
disabled={!activeId}
title="What the current app can actually see"
className={BTN}
>
Check cookies
</button>
<button onClick={pair} disabled={pairing || !browser} className={BTN_PRIMARY}>
{pairing ? <Spinner /> : config.settings.lastPairedAt ? "Pair again" : "Pair"}
</button>
<SectionHeading>Zoom</SectionHeading>
<div className="space-y-1.5">
{orderedApps.map((app) => (
<div
key={app.id}
className="flex items-center gap-3 rounded-lg border border-slate-200 px-2 py-1.5 dark:border-slate-800"
>
<Favicon url={app.url} name={app.name} />
<span className="w-28 shrink-0 truncate text-[12px]">{app.name}</span>
<input
type="range"
min={50}
max={200}
step={5}
value={Math.round((app.zoom ?? 1) * 100)}
onChange={(e) =>
run(() => api.setZoom(app.id, Number(e.target.value) / 100))
}
className="min-w-0 flex-1 cursor-pointer accent-sky-500"
/>
<span className="w-12 shrink-0 text-right font-mono text-[11px] tabular-nums text-slate-500 dark:text-slate-400">
{Math.round((app.zoom ?? 1) * 100)}%
</span>
</div>
{cookieStatus && (
<p className={`${HELP} font-mono`}>{cookieStatus}</p>
)}
{config.settings.lastPairedAt && !paired && (
<p className={HELP}>Last paired {config.settings.lastPairedAt}.</p>
)}
{paired && (
<div className="space-y-1 rounded-lg bg-sky-500/10 px-3 py-2">
<p className="text-[12px] text-sky-700 dark:text-sky-300">
Imported {paired.imported} cookie{paired.imported === 1 ? "" : "s"} across{" "}
{paired.domains} domain{paired.domains === 1 ? "" : "s"}.
</p>
{paired.domainNames.length > 0 && (
<p className="font-mono text-[11px] leading-relaxed text-slate-600 dark:text-slate-300">
{paired.domainNames.join(" ")}
</p>
)}
{paired.warnings.map((w, i) => (
<p key={i} className="text-[11px] text-slate-600 dark:text-slate-300">{w}</p>
))}
</div>
)}
<p className={HELP}>
Only cookies for the apps above and their sign-in hosts are read nothing
else leaves the browser. macOS asks for Keychain permission the first time.
Google and Microsoft tie sessions to the browser that made them, so those two
may still ask you to sign in once; after that it sticks.
</p>
</>
)}
))}
</div>
<p className={HELP}>
+ and zoom the app you are in; 0 puts it back to 100%. Each app keeps
its own size.
</p>
</section>
{/* --------------------------------------------- notifications */}
@@ -335,6 +272,7 @@ export default function Settings({
>
Check permission
</button>
<span className={HELP}>Clicking one opens the message it is about.</span>
</div>
{notifyStatus && (
<p className={`${HELP} font-mono`}>{notifyStatus}</p>