Add GitHub-style code preview + UE icon in context menu

- New CodeView: syntax-highlighted source (highlight.js, bundled offline)
  with line numbers for new files, colored unified diff for edits. Covers
  cpp/c/h, cs, py, js/ts, json/uproject, xml, ini, glsl. hljs tokens mapped
  to the app palette so it's theme-aware. Replaces the bare DiffView.
- Context-menu items now support icons; the Launch Unreal Engine item shows
  the UE mark.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-07-07 15:45:03 +03:00
parent 2feb7ca5e1
commit e9868c5b2e
6 changed files with 181 additions and 36 deletions

View File

@ -4,6 +4,7 @@ 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, fmtTime, describeFiles, leaf, saveSession, loadSession, clearSession, saveScope, loadScope, P4Info, OpenedFile, Client, Change, Session, Describe, Dir } from "./p4";
@ -253,7 +254,7 @@ function Connect({ light, toggleTheme, initial, onDone }: { light: boolean; togg
/* ---------------- main workbench ---------------- */
type Tab = "changes" | "history";
type CtxItem = { label: string; danger?: boolean; act: () => void };
type CtxItem = { label: string; danger?: boolean; icon?: ReactNode; act: () => void };
type ModalState =
| { kind: "workspaces"; items: Client[]; current: string }
@ -638,7 +639,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
<div className="vhandle grouph" onMouseDown={(e) => startResize(e, "sync")} title={t("Drag to resize")} />
<div className="tzone" onClick={() => browseTo("")} title={t("Choose the project working folder · right-click for more")}
onContextMenu={(e) => openCtx(e, [
...(uproject ? [{ label: t("Launch Unreal Engine"), act: launchUE }] : []),
...(uproject ? [{ label: t("Launch Unreal Engine"), icon: I.ue, act: launchUE }] : []),
{ label: t("Open in Explorer"), act: () => reveal(activePath || String(info?.clientRoot || "")) },
{ label: t("Choose another folder…"), act: () => browseTo("") },
{ label: t("Copy path"), act: () => copyText(activePath || "//…", t("Path")) },
@ -887,10 +888,14 @@ function ContextMenu({ x, y, items, onClose }: { x: number; y: number; items: Ct
return () => { document.removeEventListener("click", close); document.removeEventListener("scroll", close, true); document.removeEventListener("keydown", onKey); };
}, [onClose]);
const style: CSSProperties = { left: Math.min(x, window.innerWidth - 240), top: Math.min(y, window.innerHeight - (items.length * 36 + 14)) };
const hasIcon = items.some((it) => it.icon);
return (
<div className="ctxmenu" style={style} onClick={(e) => e.stopPropagation()} onContextMenu={(e) => e.preventDefault()}>
{items.map((it, i) => (
<div key={i} className={"ctxitem" + (it.danger ? " danger" : "")} onClick={() => { onClose(); it.act(); }}>{it.label}</div>
<div key={i} className={"ctxitem" + (it.danger ? " danger" : "")} onClick={() => { onClose(); it.act(); }}>
{hasIcon && <span className="ctx-ic">{it.icon}</span>}
<span>{it.label}</span>
</div>
))}
</div>
);
@ -1138,7 +1143,7 @@ function Preview({ file, onRevert }: { file?: OpenedFile; onRevert: (f: OpenedFi
{kind === "model" ? <ModelViewer depot={dp} name={name} />
: kind === "image" ? <ImageViewer depot={dp} name={name} />
: kind === "uasset" ? <UassetPreview file={file} />
: <DiffView file={file} />}
: <CodeView file={file} />}
</div>
);
}
@ -1146,35 +1151,6 @@ function Mi({ k, v, acc }: { k: string; v: string; acc?: boolean }) {
return <div className="mi"><span className="k">{k}</span><span className={"v" + (acc ? " acc" : "")}>{v}</span></div>;
}
/* ---------------- text diff ---------------- */
function DiffView({ file }: { file: OpenedFile }) {
const [text, setText] = useState<string | null>(null);
const [err, setErr] = useState("");
const dp = file.depotFile || "";
useEffect(() => {
let live = true; setText(null); setErr("");
p4.diff(dp).then((txt) => live && setText(txt)).catch((e) => live && setErr(String(e)));
return () => { live = false; };
}, [dp]);
if (err) return <div className="nofile" style={{ color: "var(--del)" }}>{err}</div>;
if (text === null) return <div className="nofile">{t("Loading diff…")}</div>;
if (!text.trim()) return <div className="nofile">{I.check}<span>{t("No text differences (or binary file).")}</span></div>;
const lines = text.split("\n");
return (
<div className="diff">
{lines.map((ln, i) => {
let cls = "";
if (ln.startsWith("@@")) cls = "hunk";
else if (ln.startsWith("+") && !ln.startsWith("+++")) cls = "add";
else if (ln.startsWith("-") && !ln.startsWith("---")) cls = "del";
return <div key={i} className={"dl " + cls}><span className="g" /><span className="c">{ln || " "}</span></div>;
})}
</div>
);
}
/* ---------------- real image viewer ---------------- */
function ImageViewer({ depot, name }: { depot: string; name: string }) {
const [url, setUrl] = useState<string | null>(null);