mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-07 18:17:38 +00:00
Shell injects the OS; Windows/Linux keep the native title bar with no traffic-light insets. Thin scrollbars on Windows/Linux so panels stop losing width to classic scrollbars.
139 lines
6.7 KiB
TypeScript
139 lines
6.7 KiB
TypeScript
// Thin bridge to the Tauri desktop shell. In the browser these are inert (isTauri() === false),
|
|
// so the SPA stays a single codebase. We use the injected `window.__TAURI__` global (the shell
|
|
// sets `withGlobalTauri`) instead of the @tauri-apps npm packages, so the browser build needs
|
|
// no Tauri dependencies.
|
|
|
|
export const isTauri = (): boolean =>
|
|
typeof (globalThis as any).__TAURI__ !== "undefined";
|
|
|
|
// "macos" | "windows" | "linux" — injected by the shell (std::env::consts::OS) before the
|
|
// SPA loads; userAgent fallback covers browser dev. The macOS overlay-titlebar layout (and
|
|
// its traffic-light compensations) must NEVER apply on Windows, which keeps its native
|
|
// title bar (alignment bug, caught on Windows 2026-07-21).
|
|
export const platformOS = (): string => {
|
|
const injected = (globalThis as any).__OCW_PLATFORM__;
|
|
if (typeof injected === "string" && injected) return injected;
|
|
return /mac/i.test(navigator.userAgent) ? "macos" : /win/i.test(navigator.userAgent) ? "windows" : "linux";
|
|
};
|
|
|
|
export type DictationStatus = {
|
|
recording: boolean;
|
|
model_installed: boolean;
|
|
model_verified: boolean;
|
|
test_passed: boolean;
|
|
download_in_progress: boolean;
|
|
model_name: string;
|
|
model_bytes: number;
|
|
supported: boolean;
|
|
device_summary: string;
|
|
compatibility_reason: string | null;
|
|
};
|
|
|
|
export type DictationDownloadProgress = {
|
|
downloaded_bytes: number;
|
|
total_bytes: number;
|
|
};
|
|
|
|
const invoke = async <T>(cmd: string, args?: Record<string, unknown>): Promise<T | null> => {
|
|
const tauri = (globalThis as any).__TAURI__;
|
|
if (!tauri?.core?.invoke) return null;
|
|
try {
|
|
return (await tauri.core.invoke(cmd, args)) as T;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const invokeStrict = async <T>(cmd: string, args?: Record<string, unknown>): Promise<T> => {
|
|
const tauri = (globalThis as any).__TAURI__;
|
|
if (!tauri?.core?.invoke) throw new Error("This feature is available in the desktop app.");
|
|
return (await tauri.core.invoke(cmd, args)) as T;
|
|
};
|
|
|
|
/** Open the native macOS folder picker (Tauri only). Returns the chosen path, or null. */
|
|
export async function pickFolder(): Promise<string | null> {
|
|
const path = await invoke<string>("pick_folder");
|
|
return typeof path === "string" && path ? path : null;
|
|
}
|
|
|
|
/** The folder picker that works EVERYWHERE: Tauri's native dialog in the desktop shell, else the
|
|
* sidecar-opened OS dialog (the sidecar is local, so the browser GUI still gets a real picker —
|
|
* owner report 2026-07-04: "Browse" was desktop-only and the browser had paste-a-path only). */
|
|
export async function chooseFolder(): Promise<string | null> {
|
|
if (isTauri()) return pickFolder();
|
|
const { pickFolderViaServer } = await import("./api");
|
|
return pickFolderViaServer();
|
|
}
|
|
|
|
/** Open-at-login (macOS LaunchAgent). */
|
|
export const getAutostart = () => invoke<boolean>("get_autostart");
|
|
export const setAutostart = (enabled: boolean) => invoke<boolean>("set_autostart", { enabled });
|
|
|
|
/** Keep this system awake so scheduled tasks fire while idle (caffeinate on macOS,
|
|
* SetThreadExecutionState on Windows). Persists across restarts. */
|
|
export const getKeepAwake = () => invoke<boolean>("get_keep_awake");
|
|
export const setKeepAwake = (enabled: boolean) => invoke<boolean>("set_keep_awake", { enabled });
|
|
|
|
/** Begin native window dragging from a custom title/header region. */
|
|
export const startWindowDrag = () => invoke<boolean>("start_window_drag");
|
|
|
|
// Local dictation is native-only. The browser build deliberately keeps this unavailable rather
|
|
// than silently sending microphone audio to a server.
|
|
export const getDictationStatus = () => invoke<DictationStatus>("get_dictation_status");
|
|
/** Instantaneous mic loudness 0..1 while recording (0 otherwise) — drives the composer's
|
|
* live waveform. Cheap; poll at ~10Hz. */
|
|
export const getDictationLevel = () => invoke<number>("dictation_level");
|
|
export const startDictation = () => invokeStrict<DictationStatus>("start_dictation");
|
|
export const stopDictation = () => invokeStrict<string>("stop_dictation");
|
|
export const cancelDictation = () => invokeStrict<void>("cancel_dictation");
|
|
export const downloadDictationModel = () => invokeStrict<DictationStatus>("download_dictation_model");
|
|
export const cancelDictationModelDownload = () => invokeStrict<void>("cancel_dictation_model_download");
|
|
export const verifyDictationModel = () => invokeStrict<DictationStatus>("verify_dictation_model");
|
|
export const markDictationTestPassed = () => invokeStrict<DictationStatus>("mark_dictation_test_passed");
|
|
export const deleteDictationModel = () => invokeStrict<DictationStatus>("delete_dictation_model");
|
|
|
|
export async function listenDictationDownloadProgress(
|
|
handler: (progress: DictationDownloadProgress) => void,
|
|
): Promise<() => void> {
|
|
const listen = (globalThis as any).__TAURI__?.event?.listen;
|
|
if (!listen) return () => {};
|
|
return (await listen("dictation-download-progress", (event: { payload: DictationDownloadProgress }) => {
|
|
handler(event.payload);
|
|
})) as () => void;
|
|
}
|
|
|
|
// --- Auto-update (desktop only; browser builds see null / throw) -----------------
|
|
|
|
export type UpdateInfo = { version: string; notes: string };
|
|
|
|
/** Ask the shell whether a newer release exists (verified manifest; see lib.rs).
|
|
* null = up to date, unreachable endpoint, or not the desktop app. */
|
|
export const checkForUpdate = () => invoke<UpdateInfo | null>("check_for_update");
|
|
|
|
/** Pre-fetch + verify the update bytes in the background so the install is instant.
|
|
* The shell caches them keyed by version; calling again for the same version is a no-op.
|
|
* Rejects when there is no update or the download fails — callers fall back to the
|
|
* download-on-install path. */
|
|
export const downloadUpdate = () => invokeStrict<void>("download_update");
|
|
|
|
/** Drop the pre-fetched update bundle (freed on "Later" so a dismissed release
|
|
* doesn't pin tens of MB for a weeks-long app run). */
|
|
export const clearPendingUpdate = () => invokeStrict<void>("clear_pending_update");
|
|
|
|
/** Install the update (pre-fetched bytes when available, else download + verify now),
|
|
* then relaunch. Resolves only on failure paths (success restarts the process on macOS;
|
|
* Windows hands off to the installer). */
|
|
export const installUpdate = () => invokeStrict<void>("install_update");
|
|
|
|
/** Best-effort open a URL in the user's browser. Uses the Tauri opener plugin if present, else
|
|
* `window.open`. The caller should also render the raw URL so it stays copyable if both no-op
|
|
* (the desktop webview has no opener plugin wired yet). */
|
|
export function openExternal(url: string): void {
|
|
const opener = (globalThis as any).__TAURI__?.opener;
|
|
if (opener?.openUrl) {
|
|
opener.openUrl(url).catch(() => window.open(url, "_blank", "noopener,noreferrer"));
|
|
return;
|
|
}
|
|
window.open(url, "_blank", "noopener,noreferrer");
|
|
}
|