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:
Bonchellon
2026-07-07 22:55:04 +03:00
parent 6c16d4a4f4
commit 0da836b0ac
61 changed files with 528 additions and 65 deletions

View File

@ -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>