Add animated startup splash + live server transfer dialog
Splash: pulsing logo, client->server packet-flow animation, cycling status steps and a shimmer bar, shown for ~1s while the session restores. Transfer progress: the backend now streams p4 sync/submit stdout line by line (throttled ~25fps) and emits p4-transfer events; the frontend shows a P4V-style progress dialog (op title, current file, file count, animated bar) that only appears once an operation runs past ~400ms. Replaces the old submit-only modal. All new strings translated (5 langs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -2,9 +2,10 @@
|
||||
// Thin wrappers around the `p4` CLI that return JSON (`-Mj`) to the frontend.
|
||||
|
||||
use serde_json::Value;
|
||||
use std::io::Write;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::time::Instant;
|
||||
use tauri::{AppHandle, Emitter, State};
|
||||
|
||||
/// App handle stashed at startup so the low-level p4 helpers can emit a live
|
||||
@ -324,11 +325,83 @@ fn run_text(conn: &Conn, args: &[&str]) -> Result<String, String> {
|
||||
Ok(if stdout.is_empty() { stderr } else { stdout })
|
||||
}
|
||||
|
||||
/// Pull a short file name out of a p4 sync/submit progress line, e.g.
|
||||
/// "//depot/Proj/Foo.uasset#3 - added as C:\..." -> "Foo.uasset".
|
||||
fn progress_file(line: &str) -> String {
|
||||
let head = line.split(" - ").next().unwrap_or(line);
|
||||
let head = head.split('#').next().unwrap_or(head);
|
||||
head.rsplit(['/', '\\']).next().unwrap_or(head).trim().to_string()
|
||||
}
|
||||
|
||||
/// Run a p4 command streaming its stdout line-by-line, emitting a `p4-transfer`
|
||||
/// event as files move so the UI can show a live P4V-style progress dialog.
|
||||
/// `op` labels the operation ("sync" | "submit"). Returns the full stdout.
|
||||
fn run_stream(conn: &Conn, args: &[&str], op: &str) -> Result<String, String> {
|
||||
let mut child = match p4_cmd(conn, false)
|
||||
.args(args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let m = format!("Failed to start p4: {e}");
|
||||
log_p4(args, 0, false, Some(&m));
|
||||
return Err(m);
|
||||
}
|
||||
};
|
||||
|
||||
let mut collected = String::new();
|
||||
let mut count: i64 = 0;
|
||||
let mut last = Instant::now();
|
||||
if let Some(out) = child.stdout.take() {
|
||||
for line in BufReader::new(out).lines().map_while(Result::ok) {
|
||||
count += 1;
|
||||
// throttle to ~25 fps regardless of file count, but always send the first few
|
||||
if count <= 5 || last.elapsed().as_millis() >= 40 {
|
||||
if let Some(app) = APP.get() {
|
||||
let _ = app.emit(
|
||||
"p4-transfer",
|
||||
serde_json::json!({ "op": op, "count": count, "file": progress_file(&line), "done": false }),
|
||||
);
|
||||
}
|
||||
last = Instant::now();
|
||||
}
|
||||
collected.push_str(&line);
|
||||
collected.push('\n');
|
||||
}
|
||||
}
|
||||
let status = child.wait().map_err(|e| e.to_string())?;
|
||||
let stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.map(|mut s| {
|
||||
let mut buf = String::new();
|
||||
let _ = std::io::Read::read_to_string(&mut s, &mut buf);
|
||||
buf.trim().to_string()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// tell the UI the transfer is finished
|
||||
if let Some(app) = APP.get() {
|
||||
let _ = app.emit(
|
||||
"p4-transfer",
|
||||
serde_json::json!({ "op": op, "count": count, "done": true }),
|
||||
);
|
||||
}
|
||||
if !status.success() && !stderr.is_empty() {
|
||||
log_p4(args, 0, false, Some(&stderr));
|
||||
return Err(stderr);
|
||||
}
|
||||
log_p4(args, count, true, None);
|
||||
Ok(collected.trim().to_string())
|
||||
}
|
||||
|
||||
/// Get Latest — sync to head, optionally scoped to a depot folder.
|
||||
#[tauri::command]
|
||||
async fn p4_sync(state: State<'_, AppState>, scope: String) -> Result<String, String> {
|
||||
let conn = current(&state)?;
|
||||
let msg = run_text(&conn, &["sync", &scope_spec(&scope)])?;
|
||||
let msg = run_stream(&conn, &["sync", &scope_spec(&scope)], "sync")?;
|
||||
Ok(if msg.is_empty() { "Already up to date — nothing to sync.".into() } else { msg })
|
||||
}
|
||||
|
||||
@ -404,7 +477,7 @@ async fn p4_commit(
|
||||
#[tauri::command]
|
||||
async fn p4_submit_change(state: State<'_, AppState>, change: String) -> Result<String, String> {
|
||||
let conn = current(&state)?;
|
||||
run_text(&conn, &["submit", "-c", &change])
|
||||
run_stream(&conn, &["submit", "-c", &change], "submit")
|
||||
}
|
||||
|
||||
/// Submit the selected files with a description.
|
||||
@ -421,7 +494,7 @@ async fn p4_submit(
|
||||
let total = run_json(&conn, &["opened"])?.len();
|
||||
// all opened files selected → submit the whole default changelist in one go
|
||||
if files.len() >= total {
|
||||
return run_text(&conn, &["submit", "-d", &description]);
|
||||
return run_stream(&conn, &["submit", "-d", &description], "submit");
|
||||
}
|
||||
// partial submit → dedicated numbered changelist
|
||||
let cl = create_changelist(&conn, &description)?;
|
||||
@ -430,7 +503,7 @@ async fn p4_submit(
|
||||
reopen.push(f.as_str());
|
||||
}
|
||||
run_text(&conn, &reopen)?;
|
||||
run_text(&conn, &["submit", "-c", &cl])
|
||||
run_stream(&conn, &["submit", "-c", &cl], "submit")
|
||||
}
|
||||
|
||||
/// Run a p4 command over a list of file paths, returning the opened records.
|
||||
|
||||
Reference in New Issue
Block a user