v0.3.2 — commit/submit progress, cancel, thumbnails, waitlist landing+admin

Client:
- Commit: batch `p4 reopen` so huge changelists (63k+) no longer blow the
  Windows command-line limit; live "N / total" progress + spinner during commit.
- Submit dialog: white arrow, "N of total" + % bar, running file log,
  Cancel button (aborts pre-commit; changelist stays pending).
- Disk scan: streaming reconcile with a live "found N files" counter + Cancel.
- Thumbnails: only decode browser-renderable image formats; glyph fallback
  (+ onError) instead of a broken-image icon for bmp/tga/dds/etc.

Landing + waitlist:
- waitlist-server: Express + SQLite email collector (dedup) with a secure
  admin panel (scrypt auth, rate-limited login, signed cookies) and CSV export.
- landing form now POSTs signups to the waitlist API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Bonchellon
2026-07-08 20:40:53 +03:00
parent 5652f489a2
commit 3954082e8d
21 changed files with 3753 additions and 144 deletions

View File

@ -102,12 +102,18 @@ export const p4 = {
del: (files: string[]) => invoke<OpenedFile[]>("p4_delete", { files }),
reconcile: (path: string) => invoke<OpenedFile[]>("p4_reconcile", { path }),
scan: (scope = "") => invoke<OpenedFile[]>("p4_scan", { scope }),
// streaming disk scan: emits `p4-scan` progress events, returns the file count.
// The caller re-reads opened() for the actual list.
scanStream: (scope = "") => invoke<number>("p4_scan_stream", { scope }),
scanCancel: () => invoke<void>("p4_scan_cancel"),
// ask an in-progress submit/sync transfer to stop (safe pre-commit for submit)
transferCancel: () => invoke<void>("p4_transfer_cancel"),
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 }),
findUproject: (scope: string) => invoke<string>("find_uproject", { scope }),
findSln: (scope: string) => invoke<string>("find_sln", { scope }),
buildSln: (path: string, config: string) => invoke<string>("build_sln", { path, config }),
buildSln: (path: string, config: string, uproject = "") => invoke<string>("build_sln", { path, config, uproject }),
launchFile: (path: string) => invoke<void>("launch_file", { path }),
listEditors: () => invoke<Editor[]>("list_editors"),
openInEditor: (depot: string, editor: string) => invoke<void>("open_in_editor", { depot, editor }),
@ -159,6 +165,10 @@ export const p4 = {
reopenTo: (change: string, files: string[]) => invoke<string>("p4_reopen_to", { change, files }),
latestChange: (scope = "") => invoke<Change | null>("p4_latest_change", { scope }),
behind: (scope = "") => invoke<{ count: number; sample: string[] }>("p4_behind", { scope }),
makeFolder: (scope: string, name: string) => invoke<string>("make_folder", { scope, name }),
deleteFolder: (path: string) => invoke<OpenedFile[]>("delete_folder", { path }),
renameFolder: (path: string, newName: string) => invoke<OpenedFile[]>("rename_folder", { path, newName }),
folderInfo: (path: string) => invoke<{ path: string; fileCount: number; size: number }>("folder_info", { path }),
// embedded Unreal .uasset thumbnail (PNG bytes), for working copy or history
uassetThumb: async (spec: string, historical: boolean): Promise<ArrayBuffer> => {
const r: unknown = await invoke("uasset_thumbnail", { spec, historical });
@ -185,7 +195,7 @@ export interface Editor { id: string; name: string; exe: string; args: string[]
export function getEditor(): string { try { return localStorage.getItem("exd-editor") || ""; } catch { return ""; } }
export function setEditorPref(id: string) { try { localStorage.setItem("exd-editor", id); } catch {} }
export interface Dir { dir?: string; [k: string]: unknown }
export interface Dir { dir?: string; local?: boolean; [k: string]: unknown }
// depot path -> short leaf name (last segment)
export function leaf(path?: string): string {
if (!path) return "";
@ -256,6 +266,17 @@ export function saveScope(client: string, path: string) {
export function loadScope(client: string): string {
try { return client ? (localStorage.getItem("exd-scope-" + client) || "") : ""; } catch { return ""; }
}
// last 3 chosen working folders (Recently) — most-recent first, per workspace
export function recentScopes(client: string): string[] {
try { const a = JSON.parse(localStorage.getItem("exd-recent-" + client) || "[]"); return Array.isArray(a) ? a.slice(0, 3) : []; } catch { return []; }
}
export function pushRecentScope(client: string, path: string) {
try {
const cur = recentScopes(client).filter((p) => p !== path);
const next = [path, ...cur].slice(0, 3);
localStorage.setItem("exd-recent-" + client, JSON.stringify(next));
} catch {}
}
// format a unix-epoch string as a short local datetime
export function fmtTime(t?: string): string {