v0.3.1 — view-panel tabs, tray + notifications, server-activity, media preview, dockable locks
- Tabbed view panel: File Viewer / File Tree / File Locks, header hidden at one tab, re-openable from Window, state persisted - Right-click Workspace/Working-folder zones -> Show in File Tree (focuses the right side) - Commit weight shown in History (p4 sizes -s //...@=CH) - Rich code editor (highlighted underlay + gutter) and clean spec-editor surfaces (no more grey notepad) - System tray (close-to-tray, tray menu Quit) + native OS notifications for teammate submits, even when hidden - 'You're behind the server' banner + native notification (p4 sync -n), rechecked only when the server advances - Audio & video preview with playback (media-src blob CSP); File Locks dockable into the view panel - Backend: depot_ls/fs_ls, p4_change_sizes, p4_behind; tauri-plugin-notification + tray-icon Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
153
src-tauri/Cargo.lock
generated
153
src-tauri/Cargo.lock
generated
@ -343,6 +343,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.0"
|
||||
@ -968,6 +974,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-updater",
|
||||
@ -1733,6 +1740,19 @@ dependencies = [
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@ -2022,6 +2042,20 @@ version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
"time",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.38.0"
|
||||
@ -2081,6 +2115,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.19.3"
|
||||
@ -2132,6 +2176,20 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891"
|
||||
dependencies = [
|
||||
"futures-lite",
|
||||
"log",
|
||||
"mac-notification-sys",
|
||||
"serde",
|
||||
"tauri-winrt-notification",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
@ -2634,6 +2692,15 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "precomputed-hash"
|
||||
version = "0.1.1"
|
||||
@ -2702,6 +2769,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.41.0"
|
||||
@ -2732,6 +2805,35 @@ version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@ -3593,6 +3695,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http",
|
||||
"image",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
@ -3747,6 +3850,25 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-notification"
|
||||
version = "2.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-opener"
|
||||
version = "2.5.4"
|
||||
@ -3912,6 +4034,17 @@ dependencies = [
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winrt-notification"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade"
|
||||
dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"windows",
|
||||
"windows-version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@ -5299,6 +5432,26 @@ dependencies = [
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.8"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "exbyte-depot"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
description = "Exbyte Depot — native Perforce client by Exbyte Studios"
|
||||
authors = ["Exbyte Studios"]
|
||||
edition = "2021"
|
||||
@ -18,8 +18,9 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||
tauri-plugin-opener = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
|
||||
@ -11,8 +11,13 @@
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-unminimize",
|
||||
"dialog:default",
|
||||
"updater:default",
|
||||
"process:default"
|
||||
"process:default",
|
||||
"notification:default"
|
||||
]
|
||||
}
|
||||
|
||||
@ -2024,10 +2024,37 @@ async fn p4_reopen_to(state: State<'_, AppState>, change: String, files: Vec<Str
|
||||
async fn p4_latest_change(state: State<'_, AppState>, scope: String) -> Result<Value, String> {
|
||||
let conn = current(&state)?;
|
||||
let spec = if scope.is_empty() { "//...".to_string() } else { scope_spec(&scope) };
|
||||
let v = run_json(&conn, &["changes", "-s", "submitted", "-m", "1", &spec])?;
|
||||
// -l → full description so notifications can show what the teammate wrote
|
||||
let v = run_json(&conn, &["changes", "-s", "submitted", "-m", "1", "-l", &spec])?;
|
||||
Ok(v.into_iter().next().unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
/// How far the workspace is behind the server: previews `p4 sync -n` and counts
|
||||
/// the files that would update (added / updated / deleted), plus a few sample
|
||||
/// names. `{count, sample}`; count 0 = up to date. Non-destructive.
|
||||
#[tauri::command]
|
||||
async fn p4_behind(state: State<'_, AppState>, scope: String) -> Result<Value, String> {
|
||||
let conn = current(&state)?;
|
||||
let spec = scope_spec(&scope);
|
||||
match run_json(&conn, &["sync", "-n", &spec]) {
|
||||
Ok(v) => {
|
||||
let files: Vec<&Value> = v.iter().filter(|o| o.get("depotFile").is_some() || o.get("clientFile").is_some()).collect();
|
||||
let sample: Vec<String> = files.iter().take(6).filter_map(|o| {
|
||||
o.get("depotFile").or_else(|| o.get("clientFile")).and_then(|s| s.as_str()).map(|s| s.rsplit(['/', '\\']).next().unwrap_or(s).to_string())
|
||||
}).collect();
|
||||
Ok(serde_json::json!({ "count": files.len(), "sample": sample }))
|
||||
}
|
||||
Err(e) => {
|
||||
let l = e.to_lowercase();
|
||||
if l.contains("up-to-date") || l.contains("no such file") || l.contains("no such area") || l.contains("not in client") {
|
||||
Ok(serde_json::json!({ "count": 0, "sample": [] }))
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Preview `p4 clean` — files that would be deleted (local extras), restored
|
||||
/// (locally deleted) or refreshed (locally modified but not opened) to make the
|
||||
/// workspace exactly match the depot. Non-destructive (`-n`).
|
||||
@ -2053,6 +2080,26 @@ 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 })
|
||||
}
|
||||
|
||||
/// Total byte weight of one or more changelists (`p4 sizes -s //scope/...@=CH`):
|
||||
/// for each change, the sum of the file revisions submitted in it. Returns
|
||||
/// `{change, size}` per input change. Used to show commit weight in History.
|
||||
#[tauri::command]
|
||||
async fn p4_change_sizes(state: State<'_, AppState>, changes: Vec<String>, scope: String) -> Result<Vec<Value>, String> {
|
||||
let conn = current(&state)?;
|
||||
let root = if scope.trim().is_empty() { "//".to_string() } else { format!("{}/", scope.trim_end_matches(['/', '\\'])) };
|
||||
let mut out: Vec<Value> = Vec::new();
|
||||
for ch in changes.iter().map(|c| c.trim()).filter(|c| !c.is_empty()) {
|
||||
let spec = format!("{}...@={}", root, ch);
|
||||
let size = run_json(&conn, &["sizes", "-s", &spec])
|
||||
.ok()
|
||||
.and_then(|v| v.into_iter().next())
|
||||
.and_then(|o| o.get("fileSize").and_then(|s| s.as_str()).and_then(|s| s.parse::<u64>().ok()))
|
||||
.unwrap_or(0);
|
||||
out.push(serde_json::json!({ "change": ch, "size": size }));
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@ -2411,6 +2458,7 @@ async fn save_ai_key(key: String) -> Result<(), String> {
|
||||
pub fn run() {
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_dialog::init());
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
@ -2419,9 +2467,56 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_process::init());
|
||||
}
|
||||
builder
|
||||
// clicking the window's X hides to the tray instead of quitting, so
|
||||
// notifications keep arriving. Real quit is the tray menu's "Quit".
|
||||
.on_window_event(|window, event| {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
use tauri::Emitter;
|
||||
if window.label() == "main" {
|
||||
let _ = window.hide();
|
||||
let _ = window.emit("tray-hidden", ());
|
||||
api.prevent_close();
|
||||
}
|
||||
}
|
||||
})
|
||||
.setup(|app| {
|
||||
use tauri::menu::{Menu, MenuItem};
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
use tauri::Manager;
|
||||
// stash the app handle so p4 helpers can emit the live command log
|
||||
let _ = APP.set(app.handle().clone());
|
||||
|
||||
// ---- system tray: keeps the app alive & notifying when the window is closed ----
|
||||
let show = MenuItem::with_id(app, "show", "Open Exbyte Depot", true, None::<&str>)?;
|
||||
let quit = MenuItem::with_id(app, "quit", "Quit Exbyte Depot", true, None::<&str>)?;
|
||||
let menu = Menu::with_items(app, &[&show, &quit])?;
|
||||
let _tray = TrayIconBuilder::with_id("main-tray")
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.tooltip("Exbyte Depot — Perforce")
|
||||
.menu(&menu)
|
||||
.show_menu_on_left_click(false)
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"show" => {
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.show();
|
||||
let _ = w.unminimize();
|
||||
let _ = w.set_focus();
|
||||
}
|
||||
}
|
||||
"quit" => app.exit(0),
|
||||
_ => {}
|
||||
})
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event {
|
||||
let app = tray.app_handle();
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.show();
|
||||
let _ = w.unminimize();
|
||||
let _ = w.set_focus();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build(app)?;
|
||||
Ok(())
|
||||
})
|
||||
.manage(AppState::default())
|
||||
@ -2514,6 +2609,8 @@ pub fn run() {
|
||||
p4_dirs,
|
||||
depot_ls,
|
||||
fs_ls,
|
||||
p4_change_sizes,
|
||||
p4_behind,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Exbyte Depot",
|
||||
"mainBinaryName": "Exbyte Depot",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"identifier": "com.bonchellon.exbyte-depot",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@ -24,8 +24,8 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost https://openrouter.ai; worker-src 'self' blob:; object-src 'none'; base-uri 'self'",
|
||||
"devCsp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost ws://localhost:1420 http://localhost:1420 https://openrouter.ai; worker-src 'self' blob:; object-src 'none'; base-uri 'self'"
|
||||
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; media-src 'self' blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost https://openrouter.ai; worker-src 'self' blob:; object-src 'none'; base-uri 'self'",
|
||||
"devCsp": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; media-src 'self' blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost ws://localhost:1420 http://localhost:1420 https://openrouter.ai; worker-src 'self' blob:; object-src 'none'; base-uri 'self'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
Reference in New Issue
Block a user