Initial commit — Exbyte Depot (native Perforce client)
Tauri 2 + React 19 + TypeScript. GitHub Desktop-style two-stage flow (local Commit -> Submit), async p4 backend, three.js asset viewer, resizable persisted panels, History revert (p4 undo), reveal-in-Explorer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
159
src/p4.ts
Normal file
159
src/p4.ts
Normal file
@ -0,0 +1,159 @@
|
||||
// Typed wrappers around the Rust p4 commands.
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface P4Info {
|
||||
serverAddress?: string;
|
||||
serverVersion?: string;
|
||||
userName?: string;
|
||||
clientName?: string;
|
||||
clientRoot?: string;
|
||||
serverUptime?: string;
|
||||
serverDate?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface OpenedFile {
|
||||
depotFile?: string;
|
||||
clientFile?: string;
|
||||
rev?: string;
|
||||
haveRev?: string;
|
||||
action?: string; // edit | add | delete | branch | integrate | move/add | move/delete
|
||||
type?: string; // text | binary | ...
|
||||
change?: string; // "default" or a number
|
||||
user?: string;
|
||||
client?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Client {
|
||||
client?: string;
|
||||
Root?: string;
|
||||
Host?: string;
|
||||
Stream?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Change {
|
||||
change?: string;
|
||||
time?: string;
|
||||
user?: string;
|
||||
client?: string;
|
||||
status?: string;
|
||||
desc?: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export const p4 = {
|
||||
clients: (port: string, user: string) =>
|
||||
invoke<Client[]>("p4_clients", { port, user }),
|
||||
login: (port: string, user: string, password: string, client: string) =>
|
||||
invoke<P4Info>("p4_login", { port, user, password, client }),
|
||||
restore: (port: string, user: string, client: string) =>
|
||||
invoke<P4Info>("p4_restore", { port, user, client }),
|
||||
info: () => invoke<P4Info>("p4_info"),
|
||||
opened: (scope = "") => invoke<OpenedFile[]>("p4_opened", { scope }),
|
||||
dirs: (path = "") => invoke<Dir[]>("p4_dirs", { path }),
|
||||
changes: () => invoke<Change[]>("p4_changes"),
|
||||
history: (path: string) => invoke<Change[]>("p4_history", { path }),
|
||||
disconnect: () => invoke<void>("p4_disconnect"),
|
||||
sync: (scope = "") => invoke<string>("p4_sync", { scope }),
|
||||
diff: (file: string) => invoke<string>("p4_diff", { file }),
|
||||
revert: (files: string[]) => invoke<string>("p4_revert", { files }),
|
||||
submit: (description: string, files: string[]) =>
|
||||
invoke<string>("p4_submit", { description, files }),
|
||||
commit: (description: string, files: string[]) =>
|
||||
invoke<string>("p4_commit", { description, files }),
|
||||
submitChange: (change: string) => invoke<string>("p4_submit_change", { change }),
|
||||
add: (files: string[]) => invoke<OpenedFile[]>("p4_add", { files }),
|
||||
edit: (files: string[]) => invoke<OpenedFile[]>("p4_edit", { files }),
|
||||
del: (files: string[]) => invoke<OpenedFile[]>("p4_delete", { files }),
|
||||
reconcile: (path: string) => invoke<OpenedFile[]>("p4_reconcile", { path }),
|
||||
scan: (scope = "") => invoke<OpenedFile[]>("p4_scan", { scope }),
|
||||
describe: (change: string) => invoke<Describe>("p4_describe", { change }),
|
||||
undoChange: (change: string) => invoke<string>("p4_undo_change", { change }),
|
||||
openInExplorer: (target: string) => invoke<void>("open_in_explorer", { target }),
|
||||
readDepot: async (depot: string): Promise<ArrayBuffer> => {
|
||||
const r: unknown = await invoke("read_depot", { depot });
|
||||
if (r instanceof ArrayBuffer) return r;
|
||||
if (ArrayBuffer.isView(r)) return (r as ArrayBufferView).buffer as ArrayBuffer;
|
||||
if (Array.isArray(r)) return new Uint8Array(r as number[]).buffer;
|
||||
return r as ArrayBuffer;
|
||||
},
|
||||
switchClient: (client: string) => invoke<P4Info>("p4_switch_client", { client }),
|
||||
};
|
||||
|
||||
export interface Dir { dir?: string; [k: string]: unknown }
|
||||
// depot path -> short leaf name (last segment)
|
||||
export function leaf(path?: string): string {
|
||||
if (!path) return "";
|
||||
const p = path.replace(/\/+$/, "");
|
||||
return p.slice(p.lastIndexOf("/") + 1);
|
||||
}
|
||||
|
||||
// a submitted changelist with indexed file fields (depotFile0, action0, ...)
|
||||
export interface Describe extends Change { [k: string]: unknown }
|
||||
export function describeFiles(d: Describe): { depotFile: string; action: string; rev: string }[] {
|
||||
const out: { depotFile: string; action: string; rev: string }[] = [];
|
||||
for (let i = 0; ; i++) {
|
||||
const dp = d[`depotFile${i}`] as string | undefined;
|
||||
if (!dp) break;
|
||||
out.push({ depotFile: dp, action: (d[`action${i}`] as string) || "edit", rev: (d[`rev${i}`] as string) || "" });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// --- session persistence (server/user/workspace — никогда не пароль) ---
|
||||
export interface Session { server: string; user: string; client: string }
|
||||
const SESSION_KEY = "exd-session";
|
||||
export function saveSession(s: Session) {
|
||||
try { localStorage.setItem(SESSION_KEY, JSON.stringify(s)); } catch {}
|
||||
}
|
||||
export function loadSession(): Session | null {
|
||||
try { const r = localStorage.getItem(SESSION_KEY); return r ? JSON.parse(r) : null; } catch { return null; }
|
||||
}
|
||||
export function clearSession() {
|
||||
try { localStorage.removeItem(SESSION_KEY); } catch {}
|
||||
}
|
||||
|
||||
// --- working-folder scope persistence (per workspace) ---
|
||||
export function saveScope(client: string, path: string) {
|
||||
try { localStorage.setItem("exd-scope-" + client, path); } catch {}
|
||||
}
|
||||
export function loadScope(client: string): string {
|
||||
try { return client ? (localStorage.getItem("exd-scope-" + client) || "") : ""; } catch { return ""; }
|
||||
}
|
||||
|
||||
// format a unix-epoch string as a short local datetime
|
||||
export function fmtTime(t?: string): string {
|
||||
if (!t) return "";
|
||||
const n = Number(t);
|
||||
if (!n) return "";
|
||||
const d = new Date(n * 1000);
|
||||
return d.toLocaleString(undefined, { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" });
|
||||
}
|
||||
|
||||
// action -> single-char status + css class
|
||||
export function statusOf(action?: string): { ch: string; cls: string } {
|
||||
const a = (action || "").toLowerCase();
|
||||
if (a.includes("add")) return { ch: "+", cls: "st-a" };
|
||||
if (a.includes("delete")) return { ch: "−", cls: "st-d" };
|
||||
return { ch: "±", cls: "st-e" }; // edit / integrate / branch / move
|
||||
}
|
||||
|
||||
// depot or client path -> { name, dir }
|
||||
export function splitPath(p?: string): { name: string; dir: string } {
|
||||
if (!p) return { name: "(unknown)", dir: "" };
|
||||
const norm = p.replace(/\\/g, "/");
|
||||
const i = norm.lastIndexOf("/");
|
||||
return { name: norm.slice(i + 1), dir: norm.slice(0, i + 1) };
|
||||
}
|
||||
|
||||
// classify by extension for the preview panel
|
||||
export type Kind = "model" | "uasset" | "image" | "code";
|
||||
export function kindOf(name: string): Kind {
|
||||
const n = name.toLowerCase();
|
||||
if (/\.(fbx|obj|gltf|glb|stl|ply|3mf|dae)$/.test(n)) return "model";
|
||||
if (/\.(uasset|umap)$/.test(n)) return "uasset";
|
||||
if (/\.(png|jpe?g|webp|gif|bmp|svg)$/.test(n)) return "image";
|
||||
return "code";
|
||||
}
|
||||
Reference in New Issue
Block a user