import { useQuery } from "@tanstack/react-query"; import { getVersion } from "@tauri-apps/api/app"; import { appDataDir, appLogDir } from "@tauri-apps/api/path"; import { Dialog, EmptyStateText, SETTINGS_TAB_GENERAL, SETTINGS_TAB_INTERFACE, SETTINGS_TAB_LICENSE, SETTINGS_TAB_THEME, type SettingsCallbacks, SettingsDialog, SettingsGeneral, SettingsInterface, SettingsTheme, } from "@yaakapp-internal/ui"; import { atom, useAtom, useSetAtom } from "jotai"; import { useEffect, useMemo, useState } from "react"; import { settingsValuesAtom } from "../lib/interfaceSettings"; import { openLink, revealPath } from "../lib/opener"; import { proxySettingsView } from "../lib/settings"; import { getOsType } from "../lib/tauri"; import { useShowToast } from "../lib/toast"; import { SettingsProxy } from "./SettingsProxy"; export const SETTINGS_TAB_PROXY = "proxy"; /** The settings window is open, on a tab, or closed */ export const settingsOpenAtom = atom(null); /** * Yaak's settings, shared: its General, Theme, Interface and License tabs, plus the proxy's own * Proxy tab. The header's gear and `⌘,` open it. */ export function SettingsWindow() { const [tab, setTab] = useAtom(settingsOpenAtom); useSettingsHotkey(); return ( setTab(null)} size="lg" noPadding noScroll className="h-[min(44rem,90vh)]" > {tab != null && } ); } /** The settings themselves, as the window holds them */ export function SettingsContent({ defaultTab }: { defaultTab: string }) { const [values, change] = useAtom(settingsValuesAtom); const app = useAppInfo(); const systemAppearance = useSystemAppearance(); const view = useMemo( () => proxySettingsView(values, app, systemAppearance), [values, app, systemAppearance], ); const showToast = useShowToast(); const callbacks = useMemo(() => { const fail = (message: string) => showToast({ message, color: "danger" }); return { onChange: (patch) => change(patch), // The proxy draws its own title bar always; the row isn't shown onApplyNativeTitlebar: (useNativeTitlebar) => change({ useNativeTitlebar }), onCheckForUpdates: () => undefined, onActivateLicense: async () => undefined, onDeactivateLicense: () => undefined, onOpenUrl: (url) => void openLink(url, fail), onRevealPath: (path) => void revealPath(path, fail), pricingUrl: (intent) => `https://yaak.app/pricing?intent=${encodeURIComponent(intent)}`, }; }, [change, showToast]); const props = { view, callbacks }; return ( , }, { value: SETTINGS_TAB_THEME, label: "Theme", icon: "palette", content: , }, { value: SETTINGS_TAB_INTERFACE, label: "Interface", icon: "columns_2", content: , }, { value: SETTINGS_TAB_PROXY, label: "Proxy", icon: "wifi", content: , }, { value: SETTINGS_TAB_LICENSE, label: "License", icon: "key_round", content: Coming soon, }, ]} /> ); } /** `⌘,`, or `Ctrl+,` off macOS, opens settings, as in Yaak */ function useSettingsHotkey() { const setOpen = useSetAtom(settingsOpenAtom); useEffect(() => { const mac = getOsType() === "macos"; function onKeyDown(e: KeyboardEvent) { if (e.key !== "," || e.shiftKey || e.altKey) return; if (mac ? !e.metaKey || e.ctrlKey : !e.ctrlKey || e.metaKey) return; e.preventDefault(); setOpen((tab) => tab ?? SETTINGS_TAB_GENERAL); } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [setOpen]); } function useAppInfo() { const os = getOsType(); const { data } = useQuery({ queryKey: ["settings_app_info"], queryFn: async () => { const [version, dataDir, logDir] = await Promise.all([ getVersion(), appDataDir(), appLogDir(), ]); return { version, dataDir, logDir }; }, retry: false, }); return useMemo( () => ({ name: "Yaak Proxy", version: data?.version ?? "", dataDir: data?.dataDir ?? "", logDir: data?.logDir ?? "", revealPathLabel: os === "macos" ? "Reveal in Finder" : os === "windows" ? "Show in Explorer" : "Show in Folder", }), [data, os], ); } function useSystemAppearance(): "light" | "dark" { const query = "(prefers-color-scheme: dark)"; const [dark, setDark] = useState(() => window.matchMedia(query).matches); useEffect(() => { const media = window.matchMedia(query); const onChange = () => setDark(media.matches); media.addEventListener("change", onChange); return () => media.removeEventListener("change", onChange); }, []); return dark ? "dark" : "light"; } /** Opens settings on a tab */ export function useOpenSettings() { const setOpen = useSetAtom(settingsOpenAtom); return (tab: string = SETTINGS_TAB_GENERAL) => setOpen(tab); }