feat: wire App + main with persist + hooks
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
68
src/App.tsx
68
src/App.tsx
@@ -1,49 +1,33 @@
|
||||
import { useState } from "react";
|
||||
import reactLogo from "./assets/react.svg";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import "./App.css";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ControlPanel } from "./components/ControlPanel";
|
||||
import { StatusBar } from "./components/StatusBar";
|
||||
import { useLiveOverlayWiring } from "./hooks/useLiveOverlayWiring";
|
||||
import { useClickThroughSync } from "./hooks/useClickThroughSync";
|
||||
import { useOverlayStore } from "./lib/store";
|
||||
import { isOverlayOpen } from "./lib/overlay";
|
||||
|
||||
function App() {
|
||||
const [greetMsg, setGreetMsg] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
function App(): React.JSX.Element {
|
||||
useLiveOverlayWiring();
|
||||
const setIsOpen = useOverlayStore((s) => s.setIsOpen);
|
||||
const [transient, setTransient] = useState<string | null>(null);
|
||||
|
||||
async function greet() {
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
setGreetMsg(await invoke("greet", { name }));
|
||||
}
|
||||
// Reconcile isOpen with reality on mount (e.g. user closed overlay via taskbar last session).
|
||||
useEffect(() => {
|
||||
void isOverlayOpen().then(setIsOpen);
|
||||
}, [setIsOpen]);
|
||||
|
||||
const handleNoOverlay = useCallback(() => {
|
||||
setTransient("No overlay to toggle");
|
||||
const t = setTimeout(() => setTransient(null), 2500);
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
useClickThroughSync(handleNoOverlay);
|
||||
|
||||
return (
|
||||
<main className="container">
|
||||
<h1>Welcome to Tauri + React</h1>
|
||||
|
||||
<div className="row">
|
||||
<a href="https://vite.dev" target="_blank">
|
||||
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://tauri.app" target="_blank">
|
||||
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
||||
|
||||
<form
|
||||
className="row"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
greet();
|
||||
}}
|
||||
>
|
||||
<input
|
||||
id="greet-input"
|
||||
onChange={(e) => setName(e.currentTarget.value)}
|
||||
placeholder="Enter a name..."
|
||||
/>
|
||||
<button type="submit">Greet</button>
|
||||
</form>
|
||||
<p>{greetMsg}</p>
|
||||
<main className="flex h-screen flex-col bg-[var(--color-bg)]">
|
||||
<ControlPanel />
|
||||
<StatusBar transient={transient} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
32
src/lib/hotkey.ts
Normal file
32
src/lib/hotkey.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useOverlayStore } from "./store";
|
||||
|
||||
type HotkeyStatus =
|
||||
| { kind: "pending" }
|
||||
| { kind: "ok" }
|
||||
| { kind: "failed"; error: string };
|
||||
|
||||
/** Polls the backend until hotkey registration is decided, then surfaces any error
|
||||
* via the store. Bounded retry: setup-time decision is essentially synchronous
|
||||
* on the Rust side, so a missed first call is at worst a few-ms wait. */
|
||||
export async function fetchHotkeyStatus(): Promise<void> {
|
||||
const maxAttempts = 20;
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
let status: HotkeyStatus;
|
||||
try {
|
||||
status = await invoke<HotkeyStatus>("get_hotkey_status");
|
||||
} catch (err) {
|
||||
console.warn("get_hotkey_status invocation failed", err);
|
||||
return;
|
||||
}
|
||||
if (status.kind === "pending") {
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
continue;
|
||||
}
|
||||
useOverlayStore.getState().setHotkeyError(
|
||||
status.kind === "failed" ? status.error : null,
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.warn("hotkey status remained pending after retries");
|
||||
}
|
||||
30
src/main.tsx
30
src/main.tsx
@@ -1,9 +1,29 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./index.css";
|
||||
import { hydrateFromDisk, startPersistSubscription, flushPendingPersistSync } from "./lib/persist";
|
||||
import { fetchHotkeyStatus } from "./lib/hotkey";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
async function bootstrap(): Promise<void> {
|
||||
await hydrateFromDisk();
|
||||
startPersistSubscription();
|
||||
// Pull the hotkey registration result. Pull-style avoids the
|
||||
// listener-not-yet-registered race that an event-based approach would have.
|
||||
void fetchHotkeyStatus();
|
||||
|
||||
// beforeunload handlers must be synchronous; we use a sync flush helper
|
||||
// that issues the write. The async save will race with window destruction
|
||||
// — whichever wins is fine, the write either lands or we retry next session.
|
||||
window.addEventListener("beforeunload", () => {
|
||||
flushPendingPersistSync();
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
void bootstrap();
|
||||
|
||||
Reference in New Issue
Block a user