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:
@ -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");
|
||||
|
||||
Reference in New Issue
Block a user