Offline Work mode + Reconcile Offline Work dialog, menu tooltips, spinner fix; v0.3.0
- Explicit Work Offline toggle (P4V-style): stops server polling, calm offline banner, persisted - Reconcile Offline Work dialog: preview adds/edits/deletes with per-file checkboxes, opens picked into a changelist - Make writable (offline edit) context action; backend p4_reconcile_preview/apply + p4_set_writable - Tooltips (title) on every top-menu item, 5 languages - Fix .ldr spinner rendering (inline-block + box-sizing) so it's a clean circle everywhere Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "exbyte-depot",
|
||||
"private": true,
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "exbyte-depot"
|
||||
version = "0.2.4"
|
||||
version = "0.3.0"
|
||||
description = "Exbyte Depot — native Perforce client by Exbyte Studios"
|
||||
authors = ["Exbyte Studios"]
|
||||
edition = "2021"
|
||||
|
||||
@ -1943,6 +1943,73 @@ async fn p4_clean_apply(state: State<'_, AppState>, scope: String) -> Result<Str
|
||||
Ok(if msg.is_empty() { "Workspace already matches the depot.".into() } else { msg })
|
||||
}
|
||||
|
||||
/// Preview offline work (`p4 reconcile -n -e -a -d`): scan the workspace on disk
|
||||
/// and report what changed while disconnected — files edited (writable but not
|
||||
/// opened), added (present locally, unknown to the depot) or deleted (in the
|
||||
/// depot but missing locally). Non-destructive (`-n`): nothing is opened yet.
|
||||
/// This is P4V's "Reconcile Offline Work". Returns one object per file with an
|
||||
/// `action` field (edit / add / delete).
|
||||
#[tauri::command]
|
||||
async fn p4_reconcile_preview(state: State<'_, AppState>, scope: String) -> Result<Vec<Value>, String> {
|
||||
let conn = current(&state)?;
|
||||
run_json(&conn, &["reconcile", "-n", "-e", "-a", "-d", &scope_spec(&scope)]).or_else(|e| {
|
||||
let l = e.to_lowercase();
|
||||
if l.contains("no file") || l.contains("up-to-date") || l.contains("no such") || l.contains("no differing") || l.contains("- no ") {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply the offline reconcile for the picked files only (`p4 reconcile -e -a -d
|
||||
/// file…`): opens the selected edits/adds/deletes into the pending changelist so
|
||||
/// they can be reviewed and submitted. Paths are the depot paths from the preview.
|
||||
#[tauri::command]
|
||||
async fn p4_reconcile_apply(state: State<'_, AppState>, paths: Vec<String>) -> Result<Vec<Value>, String> {
|
||||
let conn = current(&state)?;
|
||||
let clean: Vec<String> = paths.into_iter().map(|p| p.trim().to_string()).filter(|p| !p.is_empty()).collect();
|
||||
if clean.is_empty() {
|
||||
return Err("No files selected".into());
|
||||
}
|
||||
let mut args: Vec<&str> = vec!["reconcile", "-e", "-a", "-d"];
|
||||
for p in &clean {
|
||||
args.push(p.as_str());
|
||||
}
|
||||
run_json(&conn, &args).or_else(|e| {
|
||||
let l = e.to_lowercase();
|
||||
if l.contains("no file") || l.contains("up-to-date") || l.contains("no differing") {
|
||||
Ok(Vec::new())
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Toggle the read-only attribute on local working files so they can be edited
|
||||
/// while offline (Perforce syncs files read-only; clearing the flag lets an
|
||||
/// editor save into them before a checkout is possible). Confined to the
|
||||
/// workspace root. Reports how many files were changed.
|
||||
#[tauri::command]
|
||||
async fn p4_set_writable(state: State<'_, AppState>, paths: Vec<String>, writable: bool) -> Result<String, String> {
|
||||
let conn = current(&state)?;
|
||||
let mut done = 0usize;
|
||||
for p in paths.iter().map(|p| p.trim()).filter(|p| !p.is_empty()) {
|
||||
ensure_under_root(&conn, p)?;
|
||||
let meta = std::fs::metadata(p).map_err(|e| format!("File not found: {e}"))?;
|
||||
let mut perms = meta.permissions();
|
||||
#[allow(clippy::permissions_set_readonly_false)]
|
||||
perms.set_readonly(!writable);
|
||||
std::fs::set_permissions(p, perms).map_err(|e| format!("Cannot change {p}: {e}"))?;
|
||||
done += 1;
|
||||
}
|
||||
Ok(if writable {
|
||||
format!("{done} file(s) made writable")
|
||||
} else {
|
||||
format!("{done} file(s) set read-only")
|
||||
})
|
||||
}
|
||||
|
||||
/// Full revision history of a single file (`p4 filelog -l`), including
|
||||
/// per-revision changelist, action, user, time and description. Returns the
|
||||
/// tagged object with rev0/change0/action0/… arrays.
|
||||
@ -2301,6 +2368,9 @@ pub fn run() {
|
||||
p4_filelog,
|
||||
p4_clean_preview,
|
||||
p4_clean_apply,
|
||||
p4_reconcile_preview,
|
||||
p4_reconcile_apply,
|
||||
p4_set_writable,
|
||||
p4_submit_shelved,
|
||||
p4_submit_opts,
|
||||
p4_ignore_read,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Exbyte Depot",
|
||||
"mainBinaryName": "Exbyte Depot",
|
||||
"version": "0.2.4",
|
||||
"version": "0.3.0",
|
||||
"identifier": "com.bonchellon.exbyte-depot",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
13
src/App.css
13
src/App.css
@ -560,7 +560,7 @@ body.resizing-v{cursor:row-resize!important;user-select:none}
|
||||
display:flex;align-items:center;justify-content:center;gap:9px}
|
||||
.connect:disabled{opacity:.6;cursor:not-allowed}.connect:not(:disabled):hover{filter:brightness(1.08)}.connect svg{width:16px;height:16px}
|
||||
.spin{animation:spin 1s linear infinite}
|
||||
.ldr{width:24px;height:24px;border:2.5px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite}
|
||||
.ldr{display:inline-block;box-sizing:border-box;vertical-align:middle;width:24px;height:24px;border:2.5px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin .8s linear infinite;flex:0 0 auto}
|
||||
|
||||
/* diff view */
|
||||
.diff{flex:1;overflow:auto;min-height:0;font-family:var(--mono);font-size:12.5px;line-height:1.85;padding:6px 0}
|
||||
@ -768,8 +768,19 @@ body.resizing-v{cursor:row-resize!important;user-select:none}
|
||||
.netbar.off{background:rgba(242,99,126,.14);color:var(--del);border-bottom:1px solid rgba(242,99,126,.3)}
|
||||
.netbar.warn{background:rgba(232,176,75,.14);color:var(--edit);border-bottom:1px solid rgba(232,176,75,.3);cursor:pointer}
|
||||
.netbar.warn:hover{background:rgba(232,176,75,.2)}
|
||||
/* explicit "Work Offline" mode — violet, calmer than the error-red disconnect bar */
|
||||
.netbar.offmode{background:rgba(124,110,246,.13);color:var(--accent-2);border-bottom:1px solid rgba(124,110,246,.3)}
|
||||
.netbar-btn{margin-left:auto;border:1px solid currentColor;background:none;color:inherit;font-size:11.5px;font-weight:700;
|
||||
border-radius:7px;padding:4px 12px;cursor:pointer;font-family:var(--font)}
|
||||
.netbar-btn+.netbar-btn{margin-left:0}
|
||||
.netbar-btn:hover{background:currentColor}
|
||||
.netbar-btn:hover{color:var(--bg)}
|
||||
/* reconcile offline-work dialog */
|
||||
.rec-summary{display:flex;align-items:center;gap:6px;padding:8px 16px;font-size:12px;color:var(--muted);border-bottom:1px solid var(--border-soft)}
|
||||
.rec-all{margin-left:auto;color:var(--accent-2);font-weight:700;cursor:pointer}
|
||||
.rec-all:hover{text-decoration:underline}
|
||||
.rec-row{cursor:pointer}
|
||||
.rec-row .chk{flex:0 0 16px}
|
||||
/* "held by someone else" pill on a Changes row */
|
||||
.held{display:flex;align-items:center;gap:4px;flex:0 0 auto;font-size:10.5px;font-weight:600;color:var(--muted);
|
||||
background:var(--chip);border:1px solid var(--border-soft);border-radius:7px;padding:2px 7px;max-width:120px;overflow:hidden}
|
||||
|
||||
167
src/App.tsx
167
src/App.tsx
@ -315,6 +315,7 @@ type ModalState =
|
||||
| { kind: "streams" }
|
||||
| { kind: "jobs" }
|
||||
| { kind: "clean" }
|
||||
| { kind: "reconcile" }
|
||||
| { kind: "ignore" }
|
||||
| { kind: "filelog"; depot: string; name: string }
|
||||
| { kind: "client"; name: string; isNew: boolean }
|
||||
@ -366,6 +367,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
const [needResolve, setNeedResolve] = useState<OpenedFile[]>([]); // files awaiting conflict resolution
|
||||
const [resolveOpen, setResolveOpen] = useState(false);
|
||||
const [offline, setOffline] = useState(false); // lost connection to the server
|
||||
const [workOffline, setWorkOffline] = useState(() => { try { return localStorage.getItem("exd-workoffline") === "1"; } catch { return false; } }); // explicit offline-work mode
|
||||
const lastSubmit = useRef<string>(""); // newest submitted CL seen (new-submit toast)
|
||||
const notifPrimed = useRef(false); // don't toast on the very first poll
|
||||
const [buildLog, setBuildLog] = useState<string[]>([]); // live MSBuild output
|
||||
@ -644,6 +646,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const tick = async () => {
|
||||
if (workOffline) return; // explicit offline-work mode: don't poll the server
|
||||
try {
|
||||
const latest = await p4.latestChange(activePath);
|
||||
if (!alive) return;
|
||||
@ -668,7 +671,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
tick();
|
||||
return () => { alive = false; clearInterval(id); };
|
||||
// eslint-disable-next-line
|
||||
}, [activePath, offline, tab]);
|
||||
}, [activePath, offline, tab, workOffline]);
|
||||
|
||||
// drag & drop files/folders onto the window → reconcile them into the changelist
|
||||
useEffect(() => {
|
||||
@ -1041,6 +1044,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
{ label: t("Lock (exclusive)"), icon: I.lock, act: () => lockFiles(dps, true) },
|
||||
{ label: t("Unlock"), icon: I.unlock, act: () => lockFiles(dps, false) },
|
||||
{ label: t("Set exclusive-lock type (+l)"), icon: I.lock, act: () => setExclusiveType(dps) },
|
||||
...(workOffline ? [{ label: t("Make writable (offline edit)"), icon: I.unlock, act: () => makeWritable(targets) }] : []),
|
||||
...pending.map((cl) => ({ label: t("Move to #{n}", { n: cl.change || "" }), act: () => moveToCL(cl.change || "", dps) })),
|
||||
...(isCode ? [{ label: t("Open in {editor}", { editor: effEditorName }), icon: I.vscode, act: () => { p4.openInEditor(dp, effEditorId).then(() => flash(t("Opening in {editor}…", { editor: effEditorName }))).catch((err) => flash(String(err), true)); } }] : []),
|
||||
...(isCode ? [{ label: t("Blame (annotate)"), act: () => setModal({ kind: "blame", spec: dp, name: splitPath(dp).name }) }] : []),
|
||||
@ -1064,6 +1068,25 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
try { await p4.reopenTo(change, dps); flash(t("Moved {n} file(s) to #{c}.", { n: dps.length, c: change })); await refresh(); }
|
||||
catch (e) { flash(String(e), true); }
|
||||
}
|
||||
// toggle explicit "Work Offline" mode (P4V-style): stop polling the server;
|
||||
// edits stay local until reconciled.
|
||||
function toggleWorkOffline() {
|
||||
setWorkOffline((v) => {
|
||||
const nv = !v;
|
||||
try { localStorage.setItem("exd-workoffline", nv ? "1" : "0"); } catch {}
|
||||
if (nv) { setOffline(false); flash(t("Working offline — edits stay local. Run Reconcile Offline Work to sync.")); }
|
||||
else { flash(t("Back online.")); refresh(); }
|
||||
return nv;
|
||||
});
|
||||
}
|
||||
// clear the read-only bit on selected local files so they can be edited while
|
||||
// offline (before a checkout). Reconcile picks the changes up later.
|
||||
async function makeWritable(targets: OpenedFile[]) {
|
||||
const locals = targets.map((f) => f.clientFile || "").filter(Boolean) as string[];
|
||||
if (!locals.length) { flash(t("No local path for the selection."), true); return; }
|
||||
try { await p4.setWritable(locals, true); flash(t("{n} file(s) made writable for offline editing.", { n: locals.length })); }
|
||||
catch (e) { flash(String(e), true); }
|
||||
}
|
||||
// diff a historical revision against the one before it (History preview)
|
||||
async function showDiffPrev(file: OpenedFile) {
|
||||
const dp = file.depotFile || "";
|
||||
@ -1086,18 +1109,50 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
} catch (e) { flash(String(e), true); }
|
||||
}
|
||||
|
||||
const menus: Record<string, { label: string; kb?: string; ext?: boolean; icon?: ReactNode; act?: () => void }[]> = {
|
||||
File: [{ label: t("Switch workspace…"), icon: I.monitor, act: openWorkspaces }, { label: t("New workspace…"), icon: I.monitor, act: newWorkspace }, ...(info?.clientName ? [{ label: t("Edit current workspace…"), icon: I.gear, act: () => setModal({ kind: "client", name: info.clientName as string, isNew: false }) }] : []), { label: t("Choose working folder…"), icon: I.folder, act: () => browseTo("") }, { label: t("Disconnect"), icon: I.power, act: onDisconnect }],
|
||||
Connection: [{ label: t("Refresh"), kb: "F5", icon: I.sync, act: () => refresh() }, { label: t("Choose working folder…"), icon: I.folder, act: () => browseTo("") }],
|
||||
Actions: [{ label: t("Rescan changes"), kb: "Ctrl+R", icon: I.scan, act: rescan }, { label: t("Clean workspace…"), icon: I.revert, act: () => setModal({ kind: "clean" }) }, { label: t("Get Latest"), kb: "Ctrl+G", icon: I.sync, act: getLatest }, { label: t("Get Revision…"), icon: I.clock, act: promptSyncTo }, { label: t("Commit (local)"), icon: I.check, act: doCommit }, { label: t("Submit to server"), kb: "Ctrl+S", icon: I.up, act: pushAll }, ...(needResolve.length ? [{ label: t("Resolve conflicts ({n})", { n: needResolve.length }), icon: I.hex, act: () => setResolveOpen(true) }] : []), { label: t("Revert All…"), icon: I.revert, act: revertAll }, { label: t("Refresh"), kb: "F5", icon: I.sync, act: () => refresh() }],
|
||||
Window: [
|
||||
{ label: (dockOpen && dockTab === "log" ? "✓ " : "") + t("Log"), kb: "Ctrl+L", icon: I.log, act: () => openDock("log") },
|
||||
{ label: (dockOpen && dockTab === "terminal" ? "✓ " : "") + t("Terminal"), kb: "Ctrl+`", icon: I.terminal, act: () => openDock("terminal") },
|
||||
...(uproject ? [{ label: (dockOpen && dockTab === "unreal" ? "✓ " : "") + t("Unreal Log"), icon: I.hex, act: () => openDock("unreal") }] : []),
|
||||
{ label: t("File Locks…"), icon: I.lock, act: () => setModal({ kind: "locks" }) },
|
||||
const menus: Record<string, { label: string; kb?: string; ext?: boolean; icon?: ReactNode; hint?: string; act?: () => void }[]> = {
|
||||
File: [
|
||||
{ label: t("Switch workspace…"), icon: I.monitor, hint: t("Change which Perforce workspace (client) you're working in."), act: openWorkspaces },
|
||||
{ label: t("New workspace…"), icon: I.monitor, hint: t("Create a new workspace mapping depot folders to a local folder."), act: newWorkspace },
|
||||
...(info?.clientName ? [{ label: t("Edit current workspace…"), icon: I.gear, hint: t("Edit the current workspace spec — root folder and depot view mapping."), act: () => setModal({ kind: "client", name: info.clientName as string, isNew: false }) }] : []),
|
||||
{ label: t("Choose working folder…"), icon: I.folder, hint: t("Pick which depot folder the app shows and syncs."), act: () => browseTo("") },
|
||||
{ label: t("Disconnect"), icon: I.power, hint: t("Sign out and return to the connection screen."), act: onDisconnect },
|
||||
],
|
||||
Tools: [{ label: t("Search depot…"), icon: I.search, act: () => setModal({ kind: "search" }) }, { label: t("Exclusive Locks (typemap)…"), icon: I.lock, act: () => setModal({ kind: "typemap" }) }, { label: t("Labels…"), icon: I.clock, act: () => setModal({ kind: "labels" }) }, { label: t("Integrate / Merge / Copy…"), icon: I.branch, act: () => setModal({ kind: "branch" }) }, { label: t("Streams…"), icon: I.branch, act: () => setModal({ kind: "streams" }) }, { label: t("Jobs…"), icon: I.log, act: () => setModal({ kind: "jobs" }) }, { label: t("Edit .p4ignore…"), icon: I.gear, act: () => setModal({ kind: "ignore" }) }, { label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", icon: I.hammer, act: startBuild }, { label: t("People & Roles…"), icon: I.people, act: () => setModal({ kind: "users" }) }, { label: t("Settings…"), icon: I.gear, act: () => setModal({ kind: "settings" }) }],
|
||||
Help: [{ label: t("About"), icon: I.info, act: () => setModal({ kind: "about" }) }],
|
||||
Connection: [
|
||||
{ label: t("Refresh"), kb: "F5", icon: I.sync, hint: t("Re-read state from the server."), act: () => refresh() },
|
||||
{ label: t("Choose working folder…"), icon: I.folder, hint: t("Pick which depot folder the app shows and syncs."), act: () => browseTo("") },
|
||||
],
|
||||
Actions: [
|
||||
{ label: t("Rescan changes"), kb: "Ctrl+R", icon: I.scan, hint: t("Scan the workspace for files changed outside the app (reconcile)."), act: rescan },
|
||||
{ label: (workOffline ? "✓ " : "") + t("Work Offline"), icon: I.power, hint: t("Toggle offline-work mode: stop polling the server; edit files locally and reconcile later. Just like P4V."), act: toggleWorkOffline },
|
||||
{ label: t("Reconcile Offline Work…"), icon: I.scan, hint: t("Catch up the server with work done while offline: opens local adds / edits / deletes into a changelist so you can submit them."), act: () => setModal({ kind: "reconcile" }) },
|
||||
{ label: t("Clean workspace…"), icon: I.revert, hint: t("Make the workspace exactly match the depot — discards un-opened local changes."), act: () => setModal({ kind: "clean" }) },
|
||||
{ label: t("Get Latest"), kb: "Ctrl+G", icon: I.sync, hint: t("Sync the workspace to the newest revision (pull)."), act: getLatest },
|
||||
{ label: t("Get Revision…"), icon: I.clock, hint: t("Sync the workspace to a specific changelist, label or date."), act: promptSyncTo },
|
||||
{ label: t("Commit (local)"), icon: I.check, hint: t("Save selected files into a local pending changelist (not sent yet)."), act: doCommit },
|
||||
{ label: t("Submit to server"), kb: "Ctrl+S", icon: I.up, hint: t("Send pending changelists to the server (push)."), act: pushAll },
|
||||
...(needResolve.length ? [{ label: t("Resolve conflicts ({n})", { n: needResolve.length }), icon: I.hex, hint: t("Resolve merge conflicts left after a sync."), act: () => setResolveOpen(true) }] : []),
|
||||
{ label: t("Revert All…"), icon: I.revert, hint: t("Discard every open change and restore depot versions."), act: revertAll },
|
||||
{ label: t("Refresh"), kb: "F5", icon: I.sync, hint: t("Re-read state from the server."), act: () => refresh() },
|
||||
],
|
||||
Window: [
|
||||
{ label: (dockOpen && dockTab === "log" ? "✓ " : "") + t("Log"), kb: "Ctrl+L", icon: I.log, hint: t("Show the panel of recently run Perforce commands."), act: () => openDock("log") },
|
||||
{ label: (dockOpen && dockTab === "terminal" ? "✓ " : "") + t("Terminal"), kb: "Ctrl+`", icon: I.terminal, hint: t("Open an embedded terminal for raw p4 commands."), act: () => openDock("terminal") },
|
||||
...(uproject ? [{ label: (dockOpen && dockTab === "unreal" ? "✓ " : "") + t("Unreal Log"), icon: I.hex, hint: t("Show output from the headless Unreal preview process."), act: () => openDock("unreal") }] : []),
|
||||
{ label: t("File Locks…"), icon: I.lock, hint: t("See every file currently exclusively locked and by whom."), act: () => setModal({ kind: "locks" }) },
|
||||
],
|
||||
Tools: [
|
||||
{ label: t("Search depot…"), icon: I.search, hint: t("Find files anywhere in the depot by name or path."), act: () => setModal({ kind: "search" }) },
|
||||
{ label: t("Exclusive Locks (typemap)…"), icon: I.lock, hint: t("Set which binary asset types are exclusive-checkout (+l) so only one person edits them."), act: () => setModal({ kind: "typemap" }) },
|
||||
{ label: t("Labels…"), icon: I.clock, hint: t("Named snapshots of file revisions: tag files, or sync to a label."), act: () => setModal({ kind: "labels" }) },
|
||||
{ label: t("Integrate / Merge / Copy…"), icon: I.branch, hint: t("Move changes between branches of the depot."), act: () => setModal({ kind: "branch" }) },
|
||||
{ label: t("Streams…"), icon: I.branch, hint: t("Switch stream, merge down from the parent, or copy up to it."), act: () => setModal({ kind: "streams" }) },
|
||||
{ label: t("Jobs…"), icon: I.log, hint: t("Perforce's task/bug tracker — create jobs and attach them to changelists."), act: () => setModal({ kind: "jobs" }) },
|
||||
{ label: t("Edit .p4ignore…"), icon: I.gear, hint: t("Edit which files reconcile / add ignore (build output, caches)."), act: () => setModal({ kind: "ignore" }) },
|
||||
{ label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", icon: I.hammer, hint: t("Compile the Visual Studio solution found in the working folder."), act: startBuild },
|
||||
{ label: t("People & Roles…"), icon: I.people, hint: t("See who works on this depot and their roles."), act: () => setModal({ kind: "users" }) },
|
||||
{ label: t("Settings…"), icon: I.gear, hint: t("App preferences — language, theme, editor, and more."), act: () => setModal({ kind: "settings" }) },
|
||||
],
|
||||
Help: [{ label: t("About"), icon: I.info, hint: t("Version and information about Exbyte Depot."), act: () => setModal({ kind: "about" }) }],
|
||||
};
|
||||
|
||||
return (
|
||||
@ -1110,7 +1165,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
{t(name)}
|
||||
<div className="dropdown">
|
||||
{items.map((it) => (
|
||||
<div key={it.label} className={"di" + (it.ext ? " ext" : "")} onClick={() => it.act?.()}>
|
||||
<div key={it.label} className={"di" + (it.ext ? " ext" : "")} onClick={() => it.act?.()} title={it.hint}>
|
||||
{it.icon && <span className="di-ic">{it.icon}</span>}
|
||||
{it.label}{it.kb && <span className="kb">{it.kb}</span>}
|
||||
</div>
|
||||
@ -1164,9 +1219,16 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{offline && (
|
||||
{workOffline ? (
|
||||
<div className="netbar offmode">
|
||||
{I.power}{t("Working offline — edits stay on your disk. Reconcile to bring them into a changelist.")}
|
||||
<button className="netbar-btn" onClick={() => setModal({ kind: "reconcile" })}>{t("Reconcile…")}</button>
|
||||
<button className="netbar-btn" onClick={toggleWorkOffline}>{t("Go online")}</button>
|
||||
</div>
|
||||
) : offline && (
|
||||
<div className="netbar off">
|
||||
<span className="ldr sm" />{t("Disconnected from the server — retrying…")}
|
||||
<button className="netbar-btn" onClick={toggleWorkOffline}>{t("Work offline instead")}</button>
|
||||
</div>
|
||||
)}
|
||||
{needResolve.length > 0 && tab === "changes" && (
|
||||
@ -1359,6 +1421,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
{modal?.kind === "streams" && <StreamsModal current={String(info?.Stream || "")} onClose={() => setModal(null)} onFlash={flash} onSwitch={doSwitchStream} onInteg={doStreamInteg} />}
|
||||
{modal?.kind === "jobs" && <JobsModal onClose={() => setModal(null)} onFlash={flash} />}
|
||||
{modal?.kind === "clean" && <CleanModal scope={activePath} onClose={() => setModal(null)} onFlash={flash} onDone={() => { setModal(null); refresh(); }} />}
|
||||
{modal?.kind === "reconcile" && <ReconcileModal scope={activePath} onClose={() => setModal(null)} onFlash={flash} onDone={() => { setModal(null); refresh(); }} />}
|
||||
{modal?.kind === "ignore" && <IgnoreModal onClose={() => setModal(null)} onFlash={flash} />}
|
||||
{modal?.kind === "filelog" && <FilelogModal depot={modal.depot} name={modal.name} onClose={() => setModal(null)} onFlash={flash} onShowDiff={(title, text) => setModal({ kind: "diff", title, text })} />}
|
||||
{modal?.kind === "client" && <ClientSpecModal name={modal.name} isNew={modal.isNew} onClose={() => setModal(null)} onFlash={flash} onSaved={(c) => { setModal(null); if (modal.isNew) { switchWorkspace(c); } else { refresh(); } }} />}
|
||||
@ -2131,6 +2194,82 @@ function CleanModal({ scope, onClose, onFlash, onDone }: { scope: string; onClos
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- reconcile offline work ---------------- */
|
||||
// P4V-style "Reconcile Offline Work": scan the workspace for changes made while
|
||||
// disconnected (edits / adds / deletes) and open the picked ones into a changelist.
|
||||
function ReconcileModal({ scope, onClose, onFlash, onDone }: { scope: string; onClose: () => void; onFlash: (t: string, e?: boolean) => void; onDone: () => void }) {
|
||||
const [files, setFiles] = useState<OpenedFile[]>([]);
|
||||
const [busy, setBusy] = useState(true);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [sel, setSel] = useState<Set<string>>(new Set());
|
||||
const keyOf = (f: OpenedFile) => f.depotFile || f.clientFile || "";
|
||||
useEffect(() => {
|
||||
let live = true;
|
||||
p4.reconcilePreview(scope)
|
||||
.then((f) => { if (live) { setFiles(f); setSel(new Set(f.map(keyOf).filter(Boolean))); } })
|
||||
.catch((e) => { if (live) { onFlash(String(e), true); setFiles([]); } })
|
||||
.finally(() => live && setBusy(false));
|
||||
const k = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
||||
document.addEventListener("keydown", k);
|
||||
return () => { live = false; document.removeEventListener("keydown", k); };
|
||||
}, [scope]); // eslint-disable-line
|
||||
const toggle = (k: string) => setSel((s) => { const n = new Set(s); n.has(k) ? n.delete(k) : n.add(k); return n; });
|
||||
const allOn = files.length > 0 && sel.size === files.length;
|
||||
const toggleAll = () => setSel(allOn ? new Set() : new Set(files.map(keyOf).filter(Boolean)));
|
||||
async function apply() {
|
||||
const paths = files.map(keyOf).filter((k) => sel.has(k));
|
||||
if (!paths.length) return;
|
||||
setApplying(true);
|
||||
try {
|
||||
const opened = await p4.reconcileApply(paths);
|
||||
onFlash(t("Reconciled {n} file(s) into a changelist.", { n: opened.length || paths.length }));
|
||||
onDone();
|
||||
} catch (e) { onFlash(String(e), true); }
|
||||
finally { setApplying(false); }
|
||||
}
|
||||
// group for a readable header count
|
||||
const counts = { add: 0, edit: 0, delete: 0 } as Record<string, number>;
|
||||
for (const f of files) { const a = (f.action || "").toLowerCase(); if (a.includes("add")) counts.add++; else if (a.includes("delete")) counts.delete++; else counts.edit++; }
|
||||
return (
|
||||
<div className="modal-back" onClick={onClose}>
|
||||
<div className="picker wide" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="picker-head">{I.scan}<h3>{t("Reconcile Offline Work")}</h3><span className="ph-sub">{scope || "//…"} · {t("{n} changes", { n: files.length })}</span>
|
||||
<button className="x" onClick={onClose}><svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg></button>
|
||||
</div>
|
||||
<div className="tm-hint">{t("These files changed on disk while the server didn't know. Pick which to bring into a pending changelist: added files get p4 add, modified get p4 edit, missing get p4 delete. Nothing is submitted — you review and submit after.")}</div>
|
||||
{!busy && files.length > 0 && (
|
||||
<div className="rec-summary">
|
||||
<span style={{ color: "var(--add)", fontWeight: 700 }}>+{counts.add}</span> {t("added")} · <span style={{ color: "var(--edit)", fontWeight: 700 }}>±{counts.edit}</span> {t("modified")} · <span style={{ color: "var(--del)", fontWeight: 700 }}>−{counts.delete}</span> {t("deleted")}
|
||||
<span className="rec-all" onClick={toggleAll}>{allOn ? t("Deselect all") : t("Select all")}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="srch-list">
|
||||
{busy && <div className="ur-empty"><span className="ldr sm" /> {t("Scanning workspace for offline changes…")}</div>}
|
||||
{!busy && files.length === 0 && <div className="ur-empty">{t("Nothing to reconcile — the workspace matches what the server knows.")}</div>}
|
||||
{files.map((f) => {
|
||||
const dp = keyOf(f);
|
||||
const sp = splitPath(dp);
|
||||
const st = statusOf(f.action);
|
||||
const on = sel.has(dp);
|
||||
return (
|
||||
<div key={dp} className="srch-row rec-row" onClick={() => toggle(dp)}>
|
||||
<span className={"chk" + (on ? "" : " off")}>{on ? I.check : null}</span>
|
||||
<span className={"stat " + st.cls} style={{ marginRight: 4 }}>{st.ch}</span>
|
||||
<span className="srch-body"><span className="n">{sp.name}</span><span className="p">{sp.dir}</span></span>
|
||||
<span className="lk-who">{f.action}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="modal-actions" style={{ padding: "12px 16px", borderTop: "1px solid var(--border-soft)" }}>
|
||||
<button className="mbtn ghost" onClick={onClose} disabled={applying}>{t("Cancel")}</button>
|
||||
<button className="mbtn" onClick={apply} disabled={applying || busy || sel.size === 0}>{applying ? <span className="ldr sm" /> : null}{t("Reconcile {n} file(s)", { n: sel.size })}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- file history timeline (filelog) ---------------- */
|
||||
function FilelogModal({ depot, name, onClose, onFlash, onShowDiff }: { depot: string; name: string; onClose: () => void; onFlash: (t: string, e?: boolean) => void; onShowDiff: (title: string, text: string) => void }) {
|
||||
const [revs, setRevs] = useState<FileRev[]>([]);
|
||||
|
||||
53
src/i18n.ts
53
src/i18n.ts
@ -489,6 +489,59 @@ const D: Record<string, Tr> = {
|
||||
".p4ignore saved.": { ru: ".p4ignore сохранён.", de: ".p4ignore gespeichert.", fr: ".p4ignore enregistré.", es: ".p4ignore guardado." },
|
||||
"Pick the Unreal project working folder first.": { ru: "Сначала выбери рабочую папку Unreal-проекта.", de: "Wähle zuerst den Arbeitsordner des Unreal-Projekts.", fr: "Choisis d'abord le dossier de travail du projet Unreal.", es: "Primero elige la carpeta de trabajo del proyecto Unreal." },
|
||||
"Disconnected from the server — retrying…": { ru: "Соединение с сервером потеряно — переподключаюсь…", de: "Verbindung zum Server verloren — erneuter Versuch…", fr: "Déconnecté du serveur — nouvelle tentative…", es: "Desconectado del servidor — reintentando…" },
|
||||
// ---- offline work + reconcile ----
|
||||
"Work Offline": { ru: "Работать офлайн", de: "Offline arbeiten", fr: "Travailler hors ligne", es: "Trabajar sin conexión" },
|
||||
"Reconcile Offline Work…": { ru: "Согласовать офлайн-правки…", de: "Offline-Arbeit abgleichen…", fr: "Réconcilier le travail hors ligne…", es: "Reconciliar trabajo sin conexión…" },
|
||||
"Reconcile Offline Work": { ru: "Согласование офлайн-правок", de: "Offline-Arbeit abgleichen", fr: "Réconcilier le travail hors ligne", es: "Reconciliar trabajo sin conexión" },
|
||||
"Working offline — edits stay local. Run Reconcile Offline Work to sync.": { ru: "Работаешь офлайн — правки остаются локально. Запусти «Согласовать офлайн-правки», чтобы синхронизировать.", de: "Offline-Modus — Änderungen bleiben lokal. Führe „Offline-Arbeit abgleichen“ aus, um zu synchronisieren.", fr: "Hors ligne — les modifications restent locales. Lance « Réconcilier le travail hors ligne » pour synchroniser.", es: "Sin conexión — los cambios quedan en local. Ejecuta «Reconciliar trabajo sin conexión» para sincronizar." },
|
||||
"Back online.": { ru: "Снова онлайн.", de: "Wieder online.", fr: "De nouveau en ligne.", es: "De nuevo en línea." },
|
||||
"Working offline — edits stay on your disk. Reconcile to bring them into a changelist.": { ru: "Работаешь офлайн — правки лежат на диске. Согласуй, чтобы собрать их в changelist.", de: "Offline — Änderungen liegen auf deiner Festplatte. Gleiche ab, um sie in eine Changelist zu übernehmen.", fr: "Hors ligne — les modifications sont sur votre disque. Réconciliez pour les regrouper dans une changelist.", es: "Sin conexión — los cambios están en tu disco. Reconcilia para llevarlos a una changelist." },
|
||||
"Reconcile…": { ru: "Согласовать…", de: "Abgleichen…", fr: "Réconcilier…", es: "Reconciliar…" },
|
||||
"Go online": { ru: "Выйти онлайн", de: "Online gehen", fr: "Passer en ligne", es: "Conectar" },
|
||||
"Work offline instead": { ru: "Перейти в офлайн", de: "Stattdessen offline arbeiten", fr: "Travailler hors ligne", es: "Trabajar sin conexión" },
|
||||
"Make writable (offline edit)": { ru: "Сделать редактируемым (офлайн)", de: "Beschreibbar machen (offline)", fr: "Rendre modifiable (hors ligne)", es: "Hacer editable (sin conexión)" },
|
||||
"No local path for the selection.": { ru: "Нет локального пути для выделения.", de: "Kein lokaler Pfad für die Auswahl.", fr: "Aucun chemin local pour la sélection.", es: "No hay ruta local para la selección." },
|
||||
"{n} file(s) made writable for offline editing.": { ru: "{n} файл(ов) сделаны редактируемыми для офлайн-правки.", de: "{n} Datei(en) für Offline-Bearbeitung beschreibbar gemacht.", fr: "{n} fichier(s) rendus modifiables pour l'édition hors ligne.", es: "{n} archivo(s) hechos editables para edición sin conexión." },
|
||||
"These files changed on disk while the server didn't know. Pick which to bring into a pending changelist: added files get p4 add, modified get p4 edit, missing get p4 delete. Nothing is submitted — you review and submit after.": { ru: "Эти файлы изменились на диске без ведома сервера. Выбери, что внести в pending changelist: добавленные → p4 add, изменённые → p4 edit, пропавшие → p4 delete. Ничего не сабмитится — сначала проверишь и отправишь сам.", de: "Diese Dateien haben sich auf der Festplatte geändert, ohne dass der Server es weiß. Wähle, was in eine offene Changelist soll: neue → p4 add, geänderte → p4 edit, fehlende → p4 delete. Nichts wird übermittelt — du prüfst und übermittelst danach.", fr: "Ces fichiers ont changé sur le disque à l'insu du serveur. Choisis ceux à placer dans une changelist en attente : ajoutés → p4 add, modifiés → p4 edit, manquants → p4 delete. Rien n'est soumis — tu vérifies et soumets ensuite.", es: "Estos archivos cambiaron en disco sin que el servidor lo supiera. Elige cuáles llevar a una changelist pendiente: añadidos → p4 add, modificados → p4 edit, faltantes → p4 delete. Nada se envía — revisas y envías después." },
|
||||
"added": { ru: "добавлено", de: "hinzugefügt", fr: "ajoutés", es: "añadidos" },
|
||||
"modified": { ru: "изменено", de: "geändert", fr: "modifiés", es: "modificados" },
|
||||
"deleted": { ru: "удалено", de: "gelöscht", fr: "supprimés", es: "eliminados" },
|
||||
"Scanning workspace for offline changes…": { ru: "Сканирую воркспейс на офлайн-изменения…", de: "Arbeitsbereich wird auf Offline-Änderungen geprüft…", fr: "Analyse de l'espace de travail pour les modifications hors ligne…", es: "Analizando el espacio de trabajo en busca de cambios sin conexión…" },
|
||||
"Nothing to reconcile — the workspace matches what the server knows.": { ru: "Согласовывать нечего — воркспейс совпадает с тем, что знает сервер.", de: "Nichts abzugleichen — der Arbeitsbereich stimmt mit dem Server überein.", fr: "Rien à réconcilier — l'espace de travail correspond à ce que le serveur connaît.", es: "Nada que reconciliar — el espacio de trabajo coincide con lo que el servidor conoce." },
|
||||
"Reconcile {n} file(s)": { ru: "Согласовать {n} файл(ов)", de: "{n} Datei(en) abgleichen", fr: "Réconcilier {n} fichier(s)", es: "Reconciliar {n} archivo(s)" },
|
||||
"Reconciled {n} file(s) into a changelist.": { ru: "Согласовано {n} файл(ов) в changelist.", de: "{n} Datei(en) in eine Changelist abgeglichen.", fr: "{n} fichier(s) réconciliés dans une changelist.", es: "{n} archivo(s) reconciliados en una changelist." },
|
||||
// ---- menu tooltips ----
|
||||
"Change which Perforce workspace (client) you're working in.": { ru: "Сменить воркспейс (client), в котором ты работаешь.", de: "Wechsle den Perforce-Arbeitsbereich (Client), in dem du arbeitest.", fr: "Change l'espace de travail Perforce (client) dans lequel tu travailles.", es: "Cambia el espacio de trabajo de Perforce (client) en el que trabajas." },
|
||||
"Create a new workspace mapping depot folders to a local folder.": { ru: "Создать новый воркспейс, сопоставив папки депо с локальной папкой.", de: "Erstelle einen neuen Arbeitsbereich, der Depot-Ordner einem lokalen Ordner zuordnet.", fr: "Crée un espace de travail associant des dossiers du depot à un dossier local.", es: "Crea un espacio de trabajo que asigna carpetas del depot a una carpeta local." },
|
||||
"Edit the current workspace spec — root folder and depot view mapping.": { ru: "Изменить спеку текущего воркспейса — корневую папку и маппинг депо.", de: "Bearbeite die aktuelle Arbeitsbereich-Spezifikation — Root-Ordner und Depot-View-Mapping.", fr: "Modifie la spec de l'espace de travail actuel — dossier racine et mappage du depot.", es: "Edita la spec del espacio de trabajo actual: carpeta raíz y mapeo del depot." },
|
||||
"Pick which depot folder the app shows and syncs.": { ru: "Выбрать, какую папку депо приложение показывает и синхронизирует.", de: "Wähle, welchen Depot-Ordner die App anzeigt und synchronisiert.", fr: "Choisis quel dossier du depot l'app affiche et synchronise.", es: "Elige qué carpeta del depot muestra y sincroniza la app." },
|
||||
"Sign out and return to the connection screen.": { ru: "Выйти и вернуться к экрану подключения.", de: "Abmelden und zum Verbindungsbildschirm zurückkehren.", fr: "Se déconnecter et revenir à l'écran de connexion.", es: "Cerrar sesión y volver a la pantalla de conexión." },
|
||||
"Re-read state from the server.": { ru: "Перечитать состояние с сервера.", de: "Zustand vom Server neu einlesen.", fr: "Relire l'état depuis le serveur.", es: "Volver a leer el estado desde el servidor." },
|
||||
"Scan the workspace for files changed outside the app (reconcile).": { ru: "Просканировать воркспейс на файлы, изменённые вне приложения (reconcile).", de: "Arbeitsbereich nach außerhalb der App geänderten Dateien durchsuchen (reconcile).", fr: "Analyser l'espace de travail pour les fichiers modifiés hors de l'app (reconcile).", es: "Analiza el espacio de trabajo en busca de archivos cambiados fuera de la app (reconcile)." },
|
||||
"Toggle offline-work mode: stop polling the server; edit files locally and reconcile later. Just like P4V.": { ru: "Включить/выключить офлайн-режим: не опрашивать сервер, править файлы локально и согласовать позже. Как в P4V.", de: "Offline-Modus umschalten: Server nicht abfragen, Dateien lokal bearbeiten und später abgleichen. Wie in P4V.", fr: "Basculer le mode hors ligne : ne plus interroger le serveur, éditer localement et réconcilier plus tard. Comme dans P4V.", es: "Alternar el modo sin conexión: dejar de consultar el servidor, editar en local y reconciliar después. Como en P4V." },
|
||||
"Catch up the server with work done while offline: opens local adds / edits / deletes into a changelist so you can submit them.": { ru: "Догнать сервер работой, сделанной офлайн: открывает локальные add / edit / delete в changelist для отправки.", de: "Den Server mit offline erledigter Arbeit nachziehen: öffnet lokale Adds / Edits / Deletes in einer Changelist zum Übermitteln.", fr: "Mettre le serveur à jour avec le travail hors ligne : ouvre les ajouts / modifications / suppressions locaux dans une changelist à soumettre.", es: "Poner al día el servidor con el trabajo sin conexión: abre adds / edits / deletes locales en una changelist para enviarlos." },
|
||||
"Make the workspace exactly match the depot — discards un-opened local changes.": { ru: "Привести воркспейс точно к состоянию депо — отбрасывает неоткрытые локальные правки.", de: "Arbeitsbereich exakt an das Depot angleichen — verwirft nicht geöffnete lokale Änderungen.", fr: "Aligner exactement l'espace de travail sur le depot — abandonne les modifications locales non ouvertes.", es: "Hacer que el espacio de trabajo coincida exactamente con el depot — descarta cambios locales no abiertos." },
|
||||
"Sync the workspace to the newest revision (pull).": { ru: "Синхронизировать воркспейс до последней ревизии (pull).", de: "Arbeitsbereich auf die neueste Revision synchronisieren (Pull).", fr: "Synchroniser l'espace de travail sur la dernière révision (pull).", es: "Sincronizar el espacio de trabajo a la revisión más reciente (pull)." },
|
||||
"Sync the workspace to a specific changelist, label or date.": { ru: "Синхронизировать воркспейс до конкретного changelist, метки или даты.", de: "Arbeitsbereich auf eine bestimmte Changelist, ein Label oder ein Datum synchronisieren.", fr: "Synchroniser l'espace de travail sur une changelist, un label ou une date précis.", es: "Sincronizar el espacio de trabajo a una changelist, etiqueta o fecha concretas." },
|
||||
"Save selected files into a local pending changelist (not sent yet).": { ru: "Сохранить выбранные файлы в локальный pending changelist (пока не отправлено).", de: "Ausgewählte Dateien in eine lokale offene Changelist speichern (noch nicht gesendet).", fr: "Enregistrer les fichiers sélectionnés dans une changelist locale en attente (pas encore envoyée).", es: "Guardar los archivos seleccionados en una changelist local pendiente (aún no enviada)." },
|
||||
"Send pending changelists to the server (push).": { ru: "Отправить pending changelists на сервер (push).", de: "Offene Changelists an den Server senden (Push).", fr: "Envoyer les changelists en attente au serveur (push).", es: "Enviar las changelists pendientes al servidor (push)." },
|
||||
"Resolve merge conflicts left after a sync.": { ru: "Разрешить конфликты слияния, оставшиеся после синхронизации.", de: "Nach einer Synchronisierung verbliebene Merge-Konflikte auflösen.", fr: "Résoudre les conflits de fusion restants après une synchronisation.", es: "Resolver los conflictos de fusión que quedan tras una sincronización." },
|
||||
"Discard every open change and restore depot versions.": { ru: "Отменить все открытые правки и восстановить версии из депо.", de: "Alle offenen Änderungen verwerfen und Depot-Versionen wiederherstellen.", fr: "Abandonner toutes les modifications ouvertes et restaurer les versions du depot.", es: "Descartar todos los cambios abiertos y restaurar las versiones del depot." },
|
||||
"Show the panel of recently run Perforce commands.": { ru: "Показать панель недавно выполненных команд Perforce.", de: "Zeige das Panel der zuletzt ausgeführten Perforce-Befehle.", fr: "Afficher le panneau des commandes Perforce récemment exécutées.", es: "Mostrar el panel de comandos de Perforce ejecutados recientemente." },
|
||||
"Open an embedded terminal for raw p4 commands.": { ru: "Открыть встроенный терминал для сырых команд p4.", de: "Ein eingebettetes Terminal für rohe p4-Befehle öffnen.", fr: "Ouvrir un terminal intégré pour les commandes p4 brutes.", es: "Abrir una terminal integrada para comandos p4 en crudo." },
|
||||
"Show output from the headless Unreal preview process.": { ru: "Показать вывод фонового процесса Unreal-превью.", de: "Ausgabe des Headless-Unreal-Vorschauprozesses anzeigen.", fr: "Afficher la sortie du processus de prévisualisation Unreal sans interface.", es: "Mostrar la salida del proceso de vista previa de Unreal sin interfaz." },
|
||||
"See every file currently exclusively locked and by whom.": { ru: "Посмотреть все файлы, сейчас взятые на эксклюзивный лок, и кем.", de: "Alle aktuell exklusiv gesperrten Dateien und von wem sehen.", fr: "Voir tous les fichiers actuellement verrouillés en exclusivité et par qui.", es: "Ver todos los archivos bloqueados en exclusiva ahora mismo y por quién." },
|
||||
"Find files anywhere in the depot by name or path.": { ru: "Найти файлы в любом месте депо по имени или пути.", de: "Dateien überall im Depot nach Name oder Pfad finden.", fr: "Trouver des fichiers n'importe où dans le depot par nom ou chemin.", es: "Buscar archivos en cualquier lugar del depot por nombre o ruta." },
|
||||
"Set which binary asset types are exclusive-checkout (+l) so only one person edits them.": { ru: "Задать, какие бинарные типы ассетов берутся эксклюзивно (+l), чтобы их правил только один человек.", de: "Festlegen, welche binären Asset-Typen exklusiv ausgecheckt werden (+l), damit nur eine Person sie bearbeitet.", fr: "Définir quels types d'assets binaires sont en extraction exclusive (+l) pour qu'une seule personne les édite.", es: "Definir qué tipos de assets binarios son de extracción exclusiva (+l) para que solo una persona los edite." },
|
||||
"Named snapshots of file revisions: tag files, or sync to a label.": { ru: "Именованные снимки ревизий файлов: пометить файлы или синхронизироваться на метку.", de: "Benannte Schnappschüsse von Dateirevisionen: Dateien taggen oder auf ein Label synchronisieren.", fr: "Instantanés nommés de révisions de fichiers : taguer des fichiers ou synchroniser sur un label.", es: "Instantáneas con nombre de revisiones de archivos: etiquetar archivos o sincronizar a una etiqueta." },
|
||||
"Move changes between branches of the depot.": { ru: "Переносить изменения между ветками депо.", de: "Änderungen zwischen Depot-Branches verschieben.", fr: "Déplacer des modifications entre les branches du depot.", es: "Mover cambios entre ramas del depot." },
|
||||
"Switch stream, merge down from the parent, or copy up to it.": { ru: "Переключить стрим, влить сверху от родителя (merge down) или протолкнуть наверх (copy up).", de: "Stream wechseln, vom Parent mergen (merge down) oder nach oben kopieren (copy up).", fr: "Changer de stream, fusionner depuis le parent (merge down) ou remonter (copy up).", es: "Cambiar de stream, fusionar desde el padre (merge down) o copiar hacia arriba (copy up)." },
|
||||
"Perforce's task/bug tracker — create jobs and attach them to changelists.": { ru: "Трекер задач/багов Perforce — создавай джобы и привязывай к changelists.", de: "Perforce-Aufgaben-/Bug-Tracker — Jobs erstellen und an Changelists anhängen.", fr: "Le suivi de tâches/bugs de Perforce — crée des jobs et attache-les aux changelists.", es: "El rastreador de tareas/errores de Perforce — crea jobs y adjúntalos a changelists." },
|
||||
"Edit which files reconcile / add ignore (build output, caches).": { ru: "Изменить, какие файлы игнорируют reconcile / add (сборка, кэши).", de: "Bearbeiten, welche Dateien reconcile / add ignorieren (Build-Ausgabe, Caches).", fr: "Modifier quels fichiers reconcile / add ignorent (sortie de build, caches).", es: "Editar qué archivos ignoran reconcile / add (salida de compilación, cachés)." },
|
||||
"Compile the Visual Studio solution found in the working folder.": { ru: "Скомпилировать решение Visual Studio, найденное в рабочей папке.", de: "Die im Arbeitsordner gefundene Visual-Studio-Solution kompilieren.", fr: "Compiler la solution Visual Studio trouvée dans le dossier de travail.", es: "Compilar la solución de Visual Studio encontrada en la carpeta de trabajo." },
|
||||
"See who works on this depot and their roles.": { ru: "Посмотреть, кто работает с этим депо и их роли.", de: "Sehen, wer an diesem Depot arbeitet und welche Rollen sie haben.", fr: "Voir qui travaille sur ce depot et leurs rôles.", es: "Ver quién trabaja en este depot y sus roles." },
|
||||
"App preferences — language, theme, editor, and more.": { ru: "Настройки приложения — язык, тема, редактор и прочее.", de: "App-Einstellungen — Sprache, Design, Editor und mehr.", fr: "Préférences de l'app — langue, thème, éditeur et plus.", es: "Preferencias de la app — idioma, tema, editor y más." },
|
||||
"Version and information about Exbyte Depot.": { ru: "Версия и информация о Exbyte Depot.", de: "Version und Informationen zu Exbyte Depot.", fr: "Version et informations sur Exbyte Depot.", es: "Versión e información sobre Exbyte Depot." },
|
||||
};
|
||||
|
||||
export function t(en: string, vars?: Record<string, string | number>): string {
|
||||
|
||||
@ -74,6 +74,9 @@ export const p4 = {
|
||||
filelog: (depot: string) => invoke<Record<string, unknown>>("p4_filelog", { depot }),
|
||||
cleanPreview: (scope = "") => invoke<OpenedFile[]>("p4_clean_preview", { scope }),
|
||||
cleanApply: (scope = "") => invoke<string>("p4_clean_apply", { scope }),
|
||||
reconcilePreview: (scope = "") => invoke<OpenedFile[]>("p4_reconcile_preview", { scope }),
|
||||
reconcileApply: (paths: string[]) => invoke<OpenedFile[]>("p4_reconcile_apply", { paths }),
|
||||
setWritable: (paths: string[], writable: boolean) => invoke<string>("p4_set_writable", { paths, writable }),
|
||||
jobs: () => invoke<{ Job?: string; Status?: string; Description?: string; User?: string; [k: string]: unknown }[]>("p4_jobs"),
|
||||
fix: (job: string, change: string) => invoke<string>("p4_fix", { job, change }),
|
||||
jobSpec: (job: string) => invoke<string>("p4_job_spec", { job }),
|
||||
|
||||
Reference in New Issue
Block a user