0.2.3 — built-in code editor, .uasset previews, new icon
- Built-in code editor: edit working-copy source files in place (Edit → textarea → Save) before committing, no external IDE needed. Backend write_depot opens the file for edit, clears read-only, writes UTF-8; confined to the client root. - .uasset previews: pull the embedded Unreal thumbnail (PNG) server-side for both working copy and history; on-demand real 3D via a headless Unreal FBX export (commandlet mode, no editor window), temp file deleted right after — nothing cached on disk. Plugin content paths mapped to their mount point. - New app icon (brand mark on a theme tile) + theme-aware in-app logo. - Connection pill / Settings show the actual connected server (P4PORT), not the server's internal self-reported address. - Fix: the "select all" checkbox no longer renders as a giant empty square when not everything is selected (size pinned). - IDE picker chooses which installed editor opens code; AI replies in UI language. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -45,8 +45,9 @@ function esc(s: string): string {
|
||||
const MAX = 800_000; // don't try to render/highlight enormous files
|
||||
|
||||
// Preview source files like GitHub: syntax-highlighted content with line numbers
|
||||
// for new files, or a colored unified diff for edits.
|
||||
export default function CodeView({ file }: { file: OpenedFile }) {
|
||||
// for new files, or a colored unified diff for edits. Working-copy code files can
|
||||
// be edited in place (built-in editor) and saved before committing.
|
||||
export default function CodeView({ file, onFlash }: { file: OpenedFile; onFlash?: (text: string, err?: boolean) => void }) {
|
||||
const dp = file.depotFile || "";
|
||||
const spec = file._spec || ""; // set → read this exact submitted revision
|
||||
const { name } = splitPath(dp);
|
||||
@ -55,10 +56,14 @@ export default function CodeView({ file }: { file: OpenedFile }) {
|
||||
const [code, setCode] = useState<string | null>(null);
|
||||
const [err, setErr] = useState("");
|
||||
const [tooBig, setTooBig] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [reload, setReload] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
setDiff(null); setCode(null); setErr(""); setTooBig(false);
|
||||
setDiff(null); setCode(null); setErr(""); setTooBig(false); setEditing(false);
|
||||
// working-copy edits show a diff against the depot; historical revisions just
|
||||
// show their full content (no meaningful working diff to fetch).
|
||||
if (!spec) p4.diff(dp).then((d) => live && setDiff(d)).catch(() => live && setDiff(""));
|
||||
@ -72,7 +77,53 @@ export default function CodeView({ file }: { file: OpenedFile }) {
|
||||
setCode(new TextDecoder("utf-8", { fatal: false }).decode(bytes));
|
||||
}).catch((e) => live && setErr(String(e)));
|
||||
return () => { live = false; };
|
||||
}, [dp, spec]);
|
||||
}, [dp, spec, reload]);
|
||||
|
||||
// built-in editor: editable for a code file in the working copy (not history/binary/huge)
|
||||
const canEdit = !spec && !tooBig && err !== "binary" && code != null;
|
||||
function startEdit() { setDraft(code || ""); setEditing(true); }
|
||||
function cancelEdit() { setEditing(false); }
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
try {
|
||||
await p4.writeDepot(dp, draft);
|
||||
setCode(draft);
|
||||
setEditing(false);
|
||||
onFlash?.(t("Saved {name}", { name }));
|
||||
setReload((r) => r + 1); // refresh diff/status after the on-disk change
|
||||
} catch (e) { onFlash?.(String(e), true); }
|
||||
finally { setSaving(false); }
|
||||
}
|
||||
const pencil = <svg viewBox="0 0 24 24" width="13" height="13" fill="none"><path d="m14.5 5.5 4 4M4 20l1-4L16.5 4.5a2 2 0 0 1 3 3L8 19l-4 1Z" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" /></svg>;
|
||||
|
||||
// editing mode — plain textarea over the working copy
|
||||
if (editing) {
|
||||
return (
|
||||
<div className="right-body">
|
||||
<div className="codebar">
|
||||
<span className="cb-badge editing">{t("Editing")}</span>
|
||||
<span className="cb-name">{name}</span>
|
||||
<span className="ce-actions">
|
||||
<button className="ce-btn" onClick={cancelEdit} disabled={saving}>{t("Cancel")}</button>
|
||||
<button className="ce-btn save" onClick={save} disabled={saving || draft === code}>{saving ? <span className="ldr sm" /> : null}{t("Save")}</button>
|
||||
</span>
|
||||
</div>
|
||||
<textarea className="code-edit" value={draft} spellCheck={false} onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") cancelEdit();
|
||||
if (e.key === "s" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); if (draft !== code) save(); }
|
||||
// Tab inserts a tab instead of leaving the field
|
||||
if (e.key === "Tab") {
|
||||
e.preventDefault();
|
||||
const ta = e.currentTarget; const s = ta.selectionStart, en = ta.selectionEnd;
|
||||
const nv = draft.slice(0, s) + "\t" + draft.slice(en);
|
||||
setDraft(nv);
|
||||
requestAnimationFrame(() => { ta.selectionStart = ta.selectionEnd = s + 1; });
|
||||
}
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const lang = langOf(name);
|
||||
const highlighted = useMemo(() => {
|
||||
@ -90,7 +141,9 @@ export default function CodeView({ file }: { file: OpenedFile }) {
|
||||
const lines = diff.split("\n");
|
||||
return (
|
||||
<div className="right-body">
|
||||
<div className="codebar"><span className="cb-badge diff">{t("Diff")}</span><span className="cb-name">{name}</span></div>
|
||||
<div className="codebar"><span className="cb-badge diff">{t("Diff")}</span><span className="cb-name">{name}</span>
|
||||
{canEdit && <button className="ce-btn edit" style={{ marginLeft: "auto" }} onClick={startEdit}>{pencil}{t("Edit")}</button>}
|
||||
</div>
|
||||
<div className="diff">
|
||||
{lines.map((ln, i) => {
|
||||
let cls = "";
|
||||
@ -113,6 +166,7 @@ export default function CodeView({ file }: { file: OpenedFile }) {
|
||||
<span className={"cb-badge" + (action.includes("add") ? " add" : "")}>{action.includes("add") ? t("New file") : lang ? (LANG_LABEL[lang] || "") : t("Text")}</span>
|
||||
<span className="cb-name">{name}</span>
|
||||
<span className="cb-lines">{t("{n} lines", { n: total })}</span>
|
||||
{canEdit && <button className="ce-btn edit" onClick={startEdit}>{pencil}{t("Edit")}</button>}
|
||||
</div>
|
||||
<div className={"code" + (action.includes("add") ? " added" : "")}>
|
||||
<div className="code-gutter">{Array.from({ length: total }, (_, i) => <span key={i}>{i + 1}</span>)}</div>
|
||||
|
||||
Reference in New Issue
Block a user