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:
42
src/App.tsx
42
src/App.tsx
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user