import { useEffect, useMemo, useRef, useState } from "react";
import type { MouseEvent as ReactMouseEvent, KeyboardEvent as ReactKeyboardEvent, CSSProperties, ReactNode } from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { getCurrentWebview } from "@tauri-apps/api/webview";
import { listen } from "@tauri-apps/api/event";
import ModelViewer from "./ModelViewer";
import CodeView from "./CodeView";
import UpdateBanner from "./Updater";
import "./App.css";
import { p4, statusOf, splitPath, kindOf, isCodeFile, fmtTime, describeFiles, leaf, saveSession, loadSession, clearSession, saveScope, loadScope, fileBytes, getEditor, setEditorPref, P4Info, OpenedFile, Client, Change, Session, Describe, Dir, User, Editor } from "./p4";
import { t, LANG, LANGS, setLangGlobal, Lang } from "./i18n";
import { aiSummary, aiExplainCode, getExplain, saveExplain, dropExplain, initials, avatarColor, activity } from "./ai";
/* ---------------- window controls (frameless) ---------------- */
function WinControls() {
const w = getCurrentWindow();
return (
);
}
/* ---------------- icons ---------------- */
const I = {
hex: ,
monitor: ,
branch: ,
sync: ,
chev: ,
search: ,
check: ,
moon: ,
sun: ,
arrow: ,
spinner: ,
cube: ,
revert: ,
back: ,
chevron: ,
spark: ,
people: ,
terminal: ,
clock: ,
log: ,
vscode: ,
hammer: ,
lock: ,
unlock: ,
shelf: ,
folder: ,
power: ,
up: ,
gear: ,
info: ,
scan: ,
ue: ,
};
/* ---------------- theme hook ---------------- */
function useTheme(): [boolean, () => void] {
const [light, setLight] = useState(() => {
try { return localStorage.getItem("exd-theme") === "light"; } catch { return false; }
});
useEffect(() => {
document.body.classList.toggle("light", light);
try { localStorage.setItem("exd-theme", light ? "light" : "dark"); } catch {}
}, [light]);
return [light, () => setLight((l) => !l)];
}
/* ---------------- language hook (whole tree re-renders on change) ---------------- */
function useLang(): [Lang, (l: Lang) => void] {
const [lang, setLang] = useState(LANG);
return [lang, (l) => { setLangGlobal(l); setLang(l); }];
}
/* ---------------- UI zoom hook (scales the whole webview) ---------------- */
function useZoom(): [number, (z: number) => void] {
const [zoom, setZoom] = useState(() => {
const v = Number(localStorage.getItem("exd-zoom")); return v >= 0.5 && v <= 2 ? v : 1;
});
useEffect(() => {
(document.documentElement.style as CSSStyleDeclaration & { zoom: string }).zoom = String(zoom);
// CSS `zoom` scales layout but leaves `100vh` measuring the *unzoomed* viewport,
// so at zoom<1 a gap grows at the bottom. Expose the factor so the root can
// compensate its height (see `.win` in App.css).
document.documentElement.style.setProperty("--zoom", String(zoom));
try { localStorage.setItem("exd-zoom", String(zoom)); } catch {}
}, [zoom]);
return [zoom, (z) => setZoom(Math.min(2, Math.max(0.5, Math.round(z * 100) / 100)))];
}
/* ============================================================= */
export default function App() {
const [phase, setPhase] = useState<"loading" | "connect" | "main">("loading");
const [info, setInfo] = useState(null);
const [session, setSession] = useState(null);
const [light, toggleTheme] = useTheme();
const [lang, setLang] = useLang();
const [zoom, setZoom] = useZoom();
const saved = useMemo(() => loadSession(), []);
// try to restore the last session from the on-disk p4 ticket.
// keep the splash on screen for at least a beat so the intro animation reads.
useEffect(() => {
if (!saved) { const id = setTimeout(() => setPhase("connect"), 900); return () => clearTimeout(id); }
const min = new Promise((r) => setTimeout(r, 1000));
p4.restore(saved.server, saved.user, saved.client)
.then(async (i) => { await min; setInfo(i); setSession(saved); setPhase("main"); })
.catch(async () => { await min; setPhase("connect"); });
}, []); // eslint-disable-line
let content;
if (phase === "loading")
content = ;
else if (phase === "connect")
content = { saveSession(s); setInfo(i); setSession(s); setPhase("main"); }} />;
else
content = { setSession(s); saveSession(s); }}
onDisconnect={() => { p4.disconnect(); clearSession(); setInfo(null); setSession(null); setPhase("connect"); }} />;
return <>{content}>;
}
/* ---------------- custom styled dropdown (replaces the ugly native