diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 41d7c52..bccfc88 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,6 +6,7 @@ "permissions": [ "core:default", "opener:default", + "opener:allow-open-path", "core:window:allow-start-dragging", "core:window:allow-minimize", "core:window:allow-toggle-maximize", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 13e07a7..7c21f3c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -610,6 +610,54 @@ async fn open_in_explorer(state: State<'_, AppState>, target: String) -> Result< Ok(()) } +/// Look for an Unreal `.uproject` inside the working-folder scope (resolved to a +/// local dir via `p4 where`). Checks the folder itself and one level of subdirs. +/// Returns the local path of the first match, or "" if the scope isn't a UE project. +#[tauri::command] +async fn find_uproject(state: State<'_, AppState>, scope: String) -> Result { + if scope.is_empty() { + return Ok(String::new()); + } + let conn = current(&state)?; + let spec = format!("{}/...", scope.trim_end_matches(['/', '.'])); + let dir = match run_json(&conn, &["where", &spec])? + .into_iter() + .next() + .and_then(|v| v.get("path").and_then(|p| p.as_str()).map(String::from)) + { + Some(p) => p.trim_end_matches(['.', '/', '\\']).to_string(), + None => return Ok(String::new()), + }; + let is_uproject = |p: &std::path::Path| { + p.extension().map_or(false, |x| x.eq_ignore_ascii_case("uproject")) + }; + // the scope folder itself + if let Ok(entries) = std::fs::read_dir(&dir) { + let mut subdirs = Vec::new(); + for e in entries.flatten() { + let p = e.path(); + if is_uproject(&p) { + return Ok(p.to_string_lossy().to_string()); + } + if p.is_dir() { + subdirs.push(p); + } + } + // one level down + for sd in subdirs { + if let Ok(inner) = std::fs::read_dir(&sd) { + for e in inner.flatten() { + let p = e.path(); + if is_uproject(&p) { + return Ok(p.to_string_lossy().to_string()); + } + } + } + } + } + Ok(String::new()) +} + /// Files and metadata of a submitted changelist (History detail). #[tauri::command] async fn p4_describe(state: State<'_, AppState>, change: String) -> Result { @@ -702,6 +750,7 @@ pub fn run() { p4_scan, p4_undo_change, open_in_explorer, + find_uproject, p4_describe, read_file, read_depot, diff --git a/src/App.css b/src/App.css index 2d0f9fd..ac8ab71 100644 --- a/src/App.css +++ b/src/App.css @@ -248,6 +248,41 @@ body.resizing{cursor:col-resize!important;user-select:none} cursor:pointer;font-size:12px;font-variant-numeric:tabular-nums} .zval:hover{border-color:var(--accent)} +/* custom dropdown (replaces native ) ---------------- */ +function Select({ value, options, onChange, icon, placeholder }: + { value: string; options: { value: string; label: string; sub?: string }[]; onChange: (v: string) => void; icon?: ReactNode; placeholder?: string }) { + const [open, setOpen] = useState(false); + const ref = useRef(null); + useEffect(() => { + const onDoc = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; + const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false); + document.addEventListener("mousedown", onDoc); + document.addEventListener("keydown", onKey); + return () => { document.removeEventListener("mousedown", onDoc); document.removeEventListener("keydown", onKey); }; + }, []); + const cur = options.find((o) => o.value === value); + return ( +
+ + {open && ( +
+ {options.length === 0 &&
{placeholder}
} + {options.map((o) => ( +
{ onChange(o.value); setOpen(false); }}> + {o.value === value ? I.check : null} + {o.label}{o.sub && {o.sub}} +
+ ))} +
+ )} +
+ ); +} + /* ---------------- animated startup splash ---------------- */ function Splash({ light, toggleTheme }: { light: boolean; toggleTheme: () => void }) { const steps = [t("Connecting to server…"), t("Authenticating…"), t("Loading workspace…"), t("Syncing state…")]; @@ -193,16 +230,16 @@ function Connect({ light, toggleTheme, initial, onDone }: { light: boolean; togg setPassword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && connect()} />
-
- - {clients.length ? ( - - ) : ( + {clients.length ? ( + setClient(e.target.value)} /> - )} -
+ + )} {err &&
{err}
} + )} {pending.length > 0 ? (
diff --git a/src/i18n.ts b/src/i18n.ts index dbcd592..40a87e1 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -67,6 +67,9 @@ const D: Record = { "Get Latest": { ru: "Get Latest", de: "Get Latest", fr: "Get Latest", es: "Get Latest" }, "Working…": { ru: "Работаю…", de: "Arbeite…", fr: "En cours…", es: "Trabajando…" }, "pull from server": { ru: "забрать с сервера (pull)", de: "vom Server holen (pull)", fr: "récupérer du serveur (pull)", es: "traer del servidor (pull)" }, + "Launch": { ru: "Запустить", de: "Starten", fr: "Lancer", es: "Iniciar" }, + "Open this project in Unreal Engine": { ru: "Открыть проект в Unreal Engine", de: "Projekt in Unreal Engine öffnen", fr: "Ouvrir le projet dans Unreal Engine", es: "Abrir el proyecto en Unreal Engine" }, + "Launching Unreal…": { ru: "Запуск Unreal…", de: "Unreal wird gestartet…", fr: "Lancement d’Unreal…", es: "Iniciando Unreal…" }, // tabs / list "Changes": { ru: "Changes", de: "Änderungen", fr: "Modifications", es: "Cambios" }, diff --git a/src/p4.ts b/src/p4.ts index 6d239fb..e2c1c56 100644 --- a/src/p4.ts +++ b/src/p4.ts @@ -72,6 +72,7 @@ export const p4 = { describe: (change: string) => invoke("p4_describe", { change }), undoChange: (change: string) => invoke("p4_undo_change", { change }), openInExplorer: (target: string) => invoke("open_in_explorer", { target }), + findUproject: (scope: string) => invoke("find_uproject", { scope }), readDepot: async (depot: string): Promise => { const r: unknown = await invoke("read_depot", { depot }); if (r instanceof ArrayBuffer) return r;