English is now the default language. Added src/i18n.ts (t() helper + dictionary) covering all UI strings across App, Updater and ModelViewer, plus a language selector in Settings for English, Русский, Deutsch, Français, Español (persisted in localStorage). Backend error strings translated to English. Also removed the purple highlight on the resize handles (cursor-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
604 lines
20 KiB
Rust
604 lines
20 KiB
Rust
// Exbyte Depot — Perforce client backend.
|
|
// Thin wrappers around the `p4` CLI that return JSON (`-Mj`) to the frontend.
|
|
|
|
use serde_json::Value;
|
|
use std::io::Write;
|
|
use std::process::{Command, Stdio};
|
|
use std::sync::Mutex;
|
|
use tauri::State;
|
|
|
|
#[derive(Default, Clone)]
|
|
struct Conn {
|
|
port: String,
|
|
user: String,
|
|
client: String,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct AppState(Mutex<Conn>);
|
|
|
|
/// Build a `p4` command with the connection's global flags applied.
|
|
/// On Windows we set CREATE_NO_WINDOW so no console flashes on every call.
|
|
fn p4_cmd(conn: &Conn, tagged: bool) -> Command {
|
|
let mut c = Command::new("p4");
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
c.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
|
}
|
|
if !conn.port.is_empty() {
|
|
c.args(["-p", &conn.port]);
|
|
}
|
|
if !conn.user.is_empty() {
|
|
c.args(["-u", &conn.user]);
|
|
}
|
|
if !conn.client.is_empty() {
|
|
c.args(["-c", &conn.client]);
|
|
}
|
|
if tagged {
|
|
c.arg("-ztag").arg("-Mj"); // tagged, JSON-marshalled fields (one object per line)
|
|
}
|
|
c
|
|
}
|
|
|
|
/// Run a tagged (JSON) p4 command and collect the objects it prints.
|
|
fn run_json(conn: &Conn, args: &[&str]) -> Result<Vec<Value>, String> {
|
|
let out = p4_cmd(conn, true)
|
|
.args(args)
|
|
.output()
|
|
.map_err(|e| format!("Failed to start p4: {e}"))?;
|
|
|
|
let text = String::from_utf8_lossy(&out.stdout);
|
|
let mut items = Vec::new();
|
|
for line in text.lines() {
|
|
let t = line.trim();
|
|
if t.is_empty() {
|
|
continue;
|
|
}
|
|
let v: Value = match serde_json::from_str(t) {
|
|
Ok(v) => v,
|
|
Err(_) => continue,
|
|
};
|
|
// real error: code=="error" or severity >= 3 (E_FAILED / E_FATAL)
|
|
let is_error = v.get("code").and_then(|c| c.as_str()) == Some("error")
|
|
|| v.get("severity").and_then(|s| s.as_i64()).map_or(false, |s| s >= 3);
|
|
if is_error {
|
|
let msg = v
|
|
.get("data")
|
|
.and_then(|d| d.as_str())
|
|
.unwrap_or("unknown p4 error")
|
|
.trim()
|
|
.to_string();
|
|
return Err(msg);
|
|
}
|
|
// informational message (e.g. "File(s) not opened."), not a data record — skip
|
|
let is_message = v.get("data").is_some()
|
|
&& (v.get("generic").is_some() || v.get("severity").is_some() || v.get("level").is_some());
|
|
if is_message {
|
|
continue;
|
|
}
|
|
items.push(v);
|
|
}
|
|
if items.is_empty() && !out.status.success() {
|
|
let e = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
|
if !e.is_empty() {
|
|
return Err(e);
|
|
}
|
|
}
|
|
Ok(items)
|
|
}
|
|
|
|
/// Trust the SSL fingerprint (first-time connect to an ssl: server).
|
|
fn trust(port: &str) {
|
|
let mut c = Command::new("p4");
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
c.creation_flags(0x0800_0000);
|
|
}
|
|
let _ = c.args(["-p", port, "trust", "-y", "-f"]).output();
|
|
}
|
|
|
|
/// List workspaces (clients) for a user — used to populate the connect screen.
|
|
#[tauri::command]
|
|
async fn p4_clients(port: String, user: String) -> Result<Vec<Value>, String> {
|
|
trust(&port);
|
|
let conn = Conn {
|
|
port,
|
|
user: user.clone(),
|
|
client: String::new(),
|
|
};
|
|
run_json(&conn, &["clients", "-u", &user])
|
|
}
|
|
|
|
/// Authenticate. Runs `p4 login` feeding the password on stdin, then stores
|
|
/// the connection and returns `p4 info`.
|
|
#[tauri::command]
|
|
async fn p4_login(
|
|
state: State<'_, AppState>,
|
|
port: String,
|
|
user: String,
|
|
password: String,
|
|
client: String,
|
|
) -> Result<Value, String> {
|
|
trust(&port);
|
|
let conn = Conn {
|
|
port: port.clone(),
|
|
user: user.clone(),
|
|
client: client.clone(),
|
|
};
|
|
|
|
let mut child = p4_cmd(&conn, false)
|
|
.arg("login")
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to start p4: {e}"))?;
|
|
|
|
if let Some(si) = child.stdin.as_mut() {
|
|
si.write_all(format!("{password}\n").as_bytes())
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
let out = child.wait_with_output().map_err(|e| e.to_string())?;
|
|
if !out.status.success() {
|
|
let err = String::from_utf8_lossy(&out.stderr);
|
|
let err = if err.trim().is_empty() {
|
|
String::from_utf8_lossy(&out.stdout).trim().to_string()
|
|
} else {
|
|
err.trim().to_string()
|
|
};
|
|
return Err(if err.is_empty() {
|
|
"Login failed (wrong user or password)".into()
|
|
} else {
|
|
err
|
|
});
|
|
}
|
|
|
|
*state.0.lock().unwrap() = conn.clone();
|
|
let info = run_json(&conn, &["info"])?;
|
|
Ok(info.into_iter().next().unwrap_or(Value::Null))
|
|
}
|
|
|
|
/// Restore a saved session using the on-disk p4 ticket (no password).
|
|
/// Fails if the ticket has expired — the UI then falls back to the login form.
|
|
#[tauri::command]
|
|
async fn p4_restore(
|
|
state: State<'_, AppState>,
|
|
port: String,
|
|
user: String,
|
|
client: String,
|
|
) -> Result<Value, String> {
|
|
trust(&port);
|
|
let conn = Conn { port, user, client };
|
|
// `p4 login -s` reports ticket status; errors if not authenticated.
|
|
run_json(&conn, &["login", "-s"])?;
|
|
*state.0.lock().unwrap() = conn.clone();
|
|
let info = run_json(&conn, &["info"])?;
|
|
Ok(info.into_iter().next().unwrap_or(Value::Null))
|
|
}
|
|
|
|
fn current(state: &State<AppState>) -> Result<Conn, String> {
|
|
let c = state.0.lock().unwrap().clone();
|
|
if c.port.is_empty() {
|
|
return Err("No active connection".into());
|
|
}
|
|
Ok(c)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn p4_info(state: State<'_, AppState>) -> Result<Value, String> {
|
|
let conn = current(&state)?;
|
|
let info = run_json(&conn, &["info"])?;
|
|
Ok(info.into_iter().next().unwrap_or(Value::Null))
|
|
}
|
|
|
|
/// Build a `path/...` spec from a scope depot folder (empty = whole workspace).
|
|
fn scope_spec(scope: &str) -> String {
|
|
if scope.is_empty() {
|
|
"//...".to_string()
|
|
} else {
|
|
format!("{}/...", scope.trim_end_matches(['/', '\\']))
|
|
}
|
|
}
|
|
|
|
/// Files currently open, optionally scoped to a depot folder.
|
|
#[tauri::command]
|
|
async fn p4_opened(state: State<'_, AppState>, scope: String) -> Result<Vec<Value>, String> {
|
|
let conn = current(&state)?;
|
|
if scope.is_empty() {
|
|
run_json(&conn, &["opened"])
|
|
} else {
|
|
run_json(&conn, &["opened", &scope_spec(&scope)])
|
|
}
|
|
}
|
|
|
|
/// List sub-directories of a depot path (for the working-folder picker).
|
|
#[tauri::command]
|
|
async fn p4_dirs(state: State<'_, AppState>, path: String) -> Result<Vec<Value>, String> {
|
|
let conn = current(&state)?;
|
|
let spec = if path.is_empty() {
|
|
"//*".to_string()
|
|
} else {
|
|
format!("{}/*", path.trim_end_matches(['/', '\\']))
|
|
};
|
|
run_json(&conn, &["dirs", &spec])
|
|
}
|
|
|
|
/// Pending changelists for the current user that actually contain files.
|
|
/// Empty pending changelists (leftovers from a reverted/aborted commit) are
|
|
/// cleaned up so they never show a phantom "ready to Submit" or fail submit.
|
|
#[tauri::command]
|
|
async fn p4_changes(state: State<'_, AppState>) -> Result<Vec<Value>, String> {
|
|
let conn = current(&state)?;
|
|
let changes = run_json(
|
|
&conn,
|
|
&["changes", "-s", "pending", "-u", &conn.user, "-c", &conn.client],
|
|
)?;
|
|
// which pending changelists hold at least one opened file?
|
|
let opened = run_json(&conn, &["opened"]).unwrap_or_default();
|
|
let mut with_files = std::collections::HashSet::new();
|
|
for o in &opened {
|
|
if let Some(ch) = o.get("change").and_then(|c| c.as_str()) {
|
|
with_files.insert(ch.to_string());
|
|
}
|
|
}
|
|
let mut result = Vec::new();
|
|
for c in changes {
|
|
let num = c
|
|
.get("change")
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
if num.is_empty() {
|
|
continue;
|
|
}
|
|
if with_files.contains(&num) {
|
|
result.push(c);
|
|
} else {
|
|
// empty pending changelist — delete it (ignore failures, e.g. shelved files)
|
|
let _ = run_text(&conn, &["change", "-d", &num]);
|
|
}
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
/// Submitted history for a depot path (History tab).
|
|
#[tauri::command]
|
|
async fn p4_history(state: State<'_, AppState>, path: String) -> Result<Vec<Value>, String> {
|
|
let conn = current(&state)?;
|
|
let spec = if path.is_empty() { "//...".to_string() } else { path };
|
|
run_json(&conn, &["changes", "-s", "submitted", "-m", "50", &spec])
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn p4_disconnect(state: State<'_, AppState>) -> Result<(), String> {
|
|
*state.0.lock().unwrap() = Conn::default();
|
|
Ok(())
|
|
}
|
|
|
|
/// Run a non-tagged p4 command (raw text out). Errors carry stderr.
|
|
fn run_text(conn: &Conn, args: &[&str]) -> Result<String, String> {
|
|
let out = p4_cmd(conn, false)
|
|
.args(args)
|
|
.output()
|
|
.map_err(|e| format!("Failed to start p4: {e}"))?;
|
|
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();
|
|
if !out.status.success() && !stderr.is_empty() {
|
|
return Err(stderr);
|
|
}
|
|
Ok(if stdout.is_empty() { stderr } else { stdout })
|
|
}
|
|
|
|
/// 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)])?;
|
|
Ok(if msg.is_empty() { "Already up to date — nothing to sync.".into() } else { msg })
|
|
}
|
|
|
|
/// Unified diff of an opened file vs. the have revision.
|
|
#[tauri::command]
|
|
async fn p4_diff(state: State<'_, AppState>, file: String) -> Result<String, String> {
|
|
let conn = current(&state)?;
|
|
run_text(&conn, &["diff", "-du", &file])
|
|
}
|
|
|
|
/// Revert the given depot files (discard local edits).
|
|
#[tauri::command]
|
|
async fn p4_revert(state: State<'_, AppState>, files: Vec<String>) -> Result<String, String> {
|
|
let conn = current(&state)?;
|
|
let mut args: Vec<&str> = vec!["revert"];
|
|
for f in &files {
|
|
args.push(f.as_str());
|
|
}
|
|
run_text(&conn, &args)
|
|
}
|
|
|
|
/// Create an empty numbered changelist with a description; returns its number.
|
|
/// (`p4 submit` accepts only one file arg, so partial submits go through a
|
|
/// numbered changelist: create → reopen selected files into it → submit -c.)
|
|
fn create_changelist(conn: &Conn, desc: &str) -> Result<String, String> {
|
|
let body = desc.trim().replace('\n', "\n\t");
|
|
let spec = format!("Change:\tnew\nStatus:\tnew\nDescription:\n\t{body}\n");
|
|
let mut child = p4_cmd(conn, false)
|
|
.args(["change", "-i"])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
.map_err(|e| format!("Failed to start p4: {e}"))?;
|
|
if let Some(si) = child.stdin.as_mut() {
|
|
si.write_all(spec.as_bytes()).map_err(|e| e.to_string())?;
|
|
}
|
|
let out = child.wait_with_output().map_err(|e| e.to_string())?;
|
|
if !out.status.success() {
|
|
return Err(String::from_utf8_lossy(&out.stderr).trim().to_string());
|
|
}
|
|
// stdout: "Change 23 created."
|
|
let s = String::from_utf8_lossy(&out.stdout);
|
|
s.split_whitespace()
|
|
.find(|w| w.chars().all(|c| c.is_ascii_digit()) && !w.is_empty())
|
|
.map(|n| n.to_string())
|
|
.ok_or_else(|| format!("Failed to create changelist: {}", s.trim()))
|
|
}
|
|
|
|
/// "Commit" (local): create a pending numbered changelist with a description and
|
|
/// move the selected files into it. Nothing goes to the server yet — this is
|
|
/// revertable. Returns the changelist number.
|
|
#[tauri::command]
|
|
async fn p4_commit(
|
|
state: State<'_, AppState>,
|
|
description: String,
|
|
files: Vec<String>,
|
|
) -> Result<String, String> {
|
|
let conn = current(&state)?;
|
|
if files.is_empty() {
|
|
return Err("No files to commit".into());
|
|
}
|
|
let cl = create_changelist(&conn, &description)?;
|
|
let mut reopen: Vec<&str> = vec!["reopen", "-c", &cl];
|
|
for f in &files {
|
|
reopen.push(f.as_str());
|
|
}
|
|
run_text(&conn, &reopen)?;
|
|
Ok(cl)
|
|
}
|
|
|
|
/// Submit a numbered pending changelist to the server (the "push").
|
|
#[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])
|
|
}
|
|
|
|
/// Submit the selected files with a description.
|
|
#[tauri::command]
|
|
async fn p4_submit(
|
|
state: State<'_, AppState>,
|
|
description: String,
|
|
files: Vec<String>,
|
|
) -> Result<String, String> {
|
|
let conn = current(&state)?;
|
|
if files.is_empty() {
|
|
return Err("No files selected to submit".into());
|
|
}
|
|
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]);
|
|
}
|
|
// partial submit → dedicated numbered changelist
|
|
let cl = create_changelist(&conn, &description)?;
|
|
let mut reopen: Vec<&str> = vec!["reopen", "-c", &cl];
|
|
for f in &files {
|
|
reopen.push(f.as_str());
|
|
}
|
|
run_text(&conn, &reopen)?;
|
|
run_text(&conn, &["submit", "-c", &cl])
|
|
}
|
|
|
|
/// Run a p4 command over a list of file paths, returning the opened records.
|
|
fn files_cmd(conn: &Conn, cmd: &str, files: &[String]) -> Result<Vec<Value>, String> {
|
|
let mut args: Vec<&str> = vec![cmd];
|
|
for f in files {
|
|
args.push(f.as_str());
|
|
}
|
|
run_json(conn, &args)
|
|
}
|
|
|
|
/// Mark files for add.
|
|
#[tauri::command]
|
|
async fn p4_add(state: State<'_, AppState>, files: Vec<String>) -> Result<Vec<Value>, String> {
|
|
files_cmd(¤t(&state)?, "add", &files)
|
|
}
|
|
|
|
/// Open files for edit (check out).
|
|
#[tauri::command]
|
|
async fn p4_edit(state: State<'_, AppState>, files: Vec<String>) -> Result<Vec<Value>, String> {
|
|
files_cmd(¤t(&state)?, "edit", &files)
|
|
}
|
|
|
|
/// Mark files for delete.
|
|
#[tauri::command]
|
|
async fn p4_delete(state: State<'_, AppState>, files: Vec<String>) -> Result<Vec<Value>, String> {
|
|
files_cmd(¤t(&state)?, "delete", &files)
|
|
}
|
|
|
|
/// Reconcile a path — auto-detect adds/edits/deletes vs. the depot.
|
|
/// This is what a drag-drop of files/folders uses.
|
|
#[tauri::command]
|
|
async fn p4_reconcile(state: State<'_, AppState>, path: String) -> Result<Vec<Value>, String> {
|
|
let conn = current(&state)?;
|
|
// depot spec (//...) → recurse; local folder → recurse; local file → as-is
|
|
let spec = if path.starts_with("//") {
|
|
if path.ends_with("...") { path.clone() } else { format!("{}/...", path.trim_end_matches('/')) }
|
|
} else if std::path::Path::new(&path).is_dir() {
|
|
format!("{}/...", path.trim_end_matches(['/', '\\']))
|
|
} else {
|
|
path.clone()
|
|
};
|
|
run_json(&conn, &["reconcile", "-e", "-a", "-d", &spec])
|
|
}
|
|
|
|
/// Scan the working scope for new / changed / deleted files and open them
|
|
/// (reconcile). This is what surfaces assets created outside the client —
|
|
/// e.g. new .uasset files that Unreal wrote straight to disk but that were
|
|
/// never `p4 add`-ed, so they don't yet show up in `p4 opened`.
|
|
#[tauri::command]
|
|
async fn p4_scan(state: State<'_, AppState>, scope: String) -> Result<Vec<Value>, String> {
|
|
let conn = current(&state)?;
|
|
run_json(&conn, &["reconcile", "-e", "-a", "-d", &scope_spec(&scope)])
|
|
}
|
|
|
|
/// Undo (roll back) an already-submitted changelist. Perforce (2019.1+) does this
|
|
/// with `p4 undo`: it opens the reverting revisions, which we drop into a fresh
|
|
/// numbered changelist and submit — so the rollback is itself a new change, exactly
|
|
/// like GitHub Desktop's "Revert this commit". Returns the submit output.
|
|
#[tauri::command]
|
|
async fn p4_undo_change(state: State<'_, AppState>, change: String) -> Result<String, String> {
|
|
let conn = current(&state)?;
|
|
let cl = create_changelist(&conn, &format!("Revert change {change}"))?;
|
|
let spec = format!("//...@={change}");
|
|
// -c keeps the undone files isolated in our changelist (won't touch other open work)
|
|
run_text(&conn, &["undo", "-c", &cl, &spec])?;
|
|
run_text(&conn, &["submit", "-c", &cl])
|
|
}
|
|
|
|
/// Reveal a depot folder/file (or a raw local path) in the OS file explorer.
|
|
/// Depot paths are resolved to the local working copy via `p4 where`.
|
|
#[tauri::command]
|
|
async fn open_in_explorer(state: State<'_, AppState>, target: String) -> Result<(), String> {
|
|
let local = if target.starts_with("//") {
|
|
let conn = current(&state)?;
|
|
let spec = format!("{}/...", target.trim_end_matches(['/', '.']));
|
|
let p = run_json(&conn, &["where", &spec])?
|
|
.into_iter()
|
|
.next()
|
|
.and_then(|v| v.get("path").and_then(|x| x.as_str()).map(String::from))
|
|
.ok_or_else(|| "Could not resolve the local path".to_string())?;
|
|
// strip the trailing "..." wildcard and separators → the folder itself
|
|
p.trim_end_matches(['.', '/', '\\']).to_string()
|
|
} else {
|
|
target
|
|
};
|
|
let path = std::path::Path::new(&local);
|
|
if !path.exists() {
|
|
return Err(format!("Path not synced to disk yet: {local}"));
|
|
}
|
|
let mut c = Command::new("explorer");
|
|
#[cfg(windows)]
|
|
{
|
|
use std::os::windows::process::CommandExt;
|
|
c.creation_flags(0x0800_0000);
|
|
}
|
|
// reveal-and-select for files, open-folder for directories
|
|
if path.is_file() {
|
|
c.arg(format!("/select,{local}"));
|
|
} else {
|
|
c.arg(&local);
|
|
}
|
|
c.spawn().map_err(|e| format!("Could not open Explorer: {e}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Files and metadata of a submitted changelist (History detail).
|
|
#[tauri::command]
|
|
async fn p4_describe(state: State<'_, AppState>, change: String) -> Result<Value, String> {
|
|
let conn = current(&state)?;
|
|
let v = run_json(&conn, &["describe", "-s", &change])?;
|
|
Ok(v.into_iter().next().unwrap_or(Value::Null))
|
|
}
|
|
|
|
/// Read raw bytes of a local file — used by the 3D model / texture viewer.
|
|
#[tauri::command]
|
|
async fn read_file(path: String) -> Result<tauri::ipc::Response, String> {
|
|
std::fs::read(&path)
|
|
.map(tauri::ipc::Response::new)
|
|
.map_err(|e| format!("Could not read file: {e}"))
|
|
}
|
|
|
|
/// Read the local working-copy bytes of a depot file (resolves via `p4 where`).
|
|
#[tauri::command]
|
|
async fn read_depot(state: State<'_, AppState>, depot: String) -> Result<tauri::ipc::Response, String> {
|
|
let conn = current(&state)?;
|
|
let local = run_json(&conn, &["where", &depot])?
|
|
.into_iter()
|
|
.next()
|
|
.and_then(|v| v.get("path").and_then(|p| p.as_str()).map(String::from))
|
|
.ok_or_else(|| "Could not resolve the file path".to_string())?;
|
|
std::fs::read(&local)
|
|
.map(tauri::ipc::Response::new)
|
|
.map_err(|e| format!("Could not read file: {e}"))
|
|
}
|
|
|
|
/// Switch the active workspace (client) without re-authenticating.
|
|
#[tauri::command]
|
|
async fn p4_switch_client(state: State<'_, AppState>, client: String) -> Result<Value, String> {
|
|
let conn = {
|
|
let mut c = state.0.lock().unwrap();
|
|
if c.port.is_empty() {
|
|
return Err("No active connection".into());
|
|
}
|
|
c.client = client;
|
|
c.clone()
|
|
};
|
|
let info = run_json(&conn, &["info"])?;
|
|
Ok(info.into_iter().next().unwrap_or(Value::Null))
|
|
}
|
|
|
|
/// List depots on the server (for the Depot-path picker).
|
|
#[tauri::command]
|
|
async fn p4_depots(state: State<'_, AppState>) -> Result<Vec<Value>, String> {
|
|
let conn = current(&state)?;
|
|
run_json(&conn, &["depots"])
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
let mut builder = tauri::Builder::default()
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_dialog::init());
|
|
#[cfg(desktop)]
|
|
{
|
|
builder = builder
|
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
|
.plugin(tauri_plugin_process::init());
|
|
}
|
|
builder
|
|
.manage(AppState::default())
|
|
.invoke_handler(tauri::generate_handler![
|
|
p4_clients,
|
|
p4_login,
|
|
p4_restore,
|
|
p4_info,
|
|
p4_opened,
|
|
p4_changes,
|
|
p4_history,
|
|
p4_disconnect,
|
|
p4_sync,
|
|
p4_diff,
|
|
p4_revert,
|
|
p4_submit,
|
|
p4_commit,
|
|
p4_submit_change,
|
|
p4_add,
|
|
p4_edit,
|
|
p4_delete,
|
|
p4_reconcile,
|
|
p4_scan,
|
|
p4_undo_change,
|
|
open_in_explorer,
|
|
p4_describe,
|
|
read_file,
|
|
read_depot,
|
|
p4_switch_client,
|
|
p4_depots,
|
|
p4_dirs,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|