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:
Bonchellon
2026-07-08 14:26:22 +03:00
parent 9c24d45bf9
commit 5652f489a2
12 changed files with 689 additions and 126 deletions

View File

@ -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");