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:
+15
-1
@@ -6,7 +6,7 @@ import Nav from "./components/Nav";
|
||||
import Settings from "./components/Settings";
|
||||
import { BTN_PRIMARY } from "./components/ui";
|
||||
import { useAppearance, type Theme } from "./hooks/useAppearance";
|
||||
import type { Config, Group, HiddenEvent, SwitchEvent } from "./types";
|
||||
import type { Config, Group, HiddenEvent, NotificationClick, SwitchEvent } from "./types";
|
||||
|
||||
export default function App() {
|
||||
const [config, setConfig] = useState<Config | null>(null);
|
||||
@@ -89,6 +89,20 @@ export default function App() {
|
||||
setFocusHidden(e.payload);
|
||||
setSettingsOpen(true);
|
||||
}),
|
||||
// A macOS banner was clicked. Switch to the app that raised it, then let
|
||||
// that page's own handler run — it is the only thing that knows which
|
||||
// message the notification was about.
|
||||
listen<NotificationClick>("notification-clicked", async (e) => {
|
||||
await api.focusWindow();
|
||||
setSettingsOpen(false);
|
||||
setActiveId(e.payload.appId);
|
||||
await api.setActive(e.payload.appId);
|
||||
await api.notificationClick(e.payload.appId, e.payload.notificationId);
|
||||
}),
|
||||
// Zoom changed from the menu bar; keep Settings' sliders honest.
|
||||
listen<[string, number]>("zoom-changed", () => {
|
||||
void api.getConfig().then(setConfig);
|
||||
}),
|
||||
];
|
||||
return () => {
|
||||
unlisten.forEach((p) => p.then((f) => f()));
|
||||
|
||||
+7
-6
@@ -1,7 +1,7 @@
|
||||
/** Every call into Rust, in one place. */
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import type { Browser, Config, Group, PairResult, WorkApp } from "./types";
|
||||
import type { Config, Group, WorkApp } from "./types";
|
||||
|
||||
export const getConfig = () => invoke<Config>("get_config");
|
||||
export const bootstrap = () => invoke<void>("bootstrap");
|
||||
@@ -40,11 +40,12 @@ export const setHidden = (appId: string, hidden: string[]) =>
|
||||
invoke<Config>("set_hidden", { appId, hidden });
|
||||
export const pickHidden = (appId: string) => invoke<void>("pick_hidden", { appId });
|
||||
|
||||
export const listBrowsers = () => invoke<Browser[]>("list_browsers");
|
||||
export const pairBrowser = (browser: string) =>
|
||||
invoke<PairResult>("pair_browser", { browser });
|
||||
export const testNotification = (appId: string) =>
|
||||
invoke<void>("test_notification", { appId });
|
||||
export const notificationStatus = () => invoke<string>("notification_status");
|
||||
export const probeCookies = (appId: string) => invoke<void>("probe_cookies", { appId });
|
||||
export const cookieProbe = () => invoke<string>("cookie_probe");
|
||||
|
||||
export const setZoom = (appId: string, zoom: number) =>
|
||||
invoke<Config>("set_zoom", { appId, zoom });
|
||||
export const notificationClick = (appId: string, notificationId: string) =>
|
||||
invoke<void>("notification_click", { appId, notificationId });
|
||||
export const focusWindow = () => invoke<void>("focus_window");
|
||||
|
||||
@@ -137,14 +137,6 @@ export default function Nav({
|
||||
>
|
||||
<Favicon url={app.url} name={app.name} />
|
||||
<span className="truncate">{app.name}</span>
|
||||
{app.hidden.length > 0 && (
|
||||
<span
|
||||
title={`${app.hidden.length} element${app.hidden.length === 1 ? "" : "s"} hidden here`}
|
||||
className="ml-auto font-mono text-[10px] opacity-50"
|
||||
>
|
||||
{app.hidden.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
|
||||
+32
-94
@@ -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>
|
||||
|
||||
+8
-13
@@ -7,6 +7,8 @@ export interface WorkApp {
|
||||
userAgent: string | null;
|
||||
/** CSS selectors this app hides on every page. */
|
||||
hidden: string[];
|
||||
/** Page zoom, remembered per app. */
|
||||
zoom: number;
|
||||
order: number;
|
||||
}
|
||||
|
||||
@@ -20,8 +22,6 @@ export interface Group {
|
||||
export interface Settings {
|
||||
navCollapsed: boolean;
|
||||
theme: "system" | "light" | "dark";
|
||||
pairedBrowser: string | null;
|
||||
lastPairedAt: string | null;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@@ -42,21 +42,16 @@ export interface UrlEvent {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Browser {
|
||||
id: string;
|
||||
label: string;
|
||||
available: boolean;
|
||||
}
|
||||
|
||||
export interface PairResult {
|
||||
imported: number;
|
||||
domains: number;
|
||||
domainNames: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/** Rust recorded a selector the user right-clicked away. */
|
||||
export interface HiddenEvent {
|
||||
appId: string;
|
||||
selector: string;
|
||||
}
|
||||
|
||||
/** The user clicked a macOS banner an app raised. */
|
||||
export interface NotificationClick {
|
||||
appId: string;
|
||||
notificationId: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user