Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc88269baf | |||
| 9fc81015d7 | |||
| 294d10eb40 |
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "exbyte-depot",
|
"name": "exbyte-depot",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.3.6",
|
"version": "0.3.9",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@ -1,28 +1,32 @@
|
|||||||
// Unreal Engine Tools — official plugin.
|
// Unreal Engine Tools — official plugin.
|
||||||
// The core app keeps the build engine (UnrealBuildTool / MSBuild), the build
|
// The core app keeps the build engine (UnrealBuildTool / MSBuild), the build
|
||||||
// overlay and the log reader; this plugin is just the Unreal-facing surface:
|
// overlay and the log reader; this plugin is just the Unreal-facing surface.
|
||||||
// a "Build Solution" action and a live Unreal-log view. Reached through the
|
// It renders natively: the actions sit in the working-folder right-click menu
|
||||||
// permission-gated `host.ue` bridge.
|
// with the host's Unreal + hammer icons, and the log is a bottom-dock tab —
|
||||||
|
// exactly where these lived before, just delivered as a plugin. Reached through
|
||||||
|
// the permission-gated `host.ue` bridge.
|
||||||
|
|
||||||
export function activate(host) {
|
export function activate(host) {
|
||||||
const ue = host.ue;
|
const ue = host.ue;
|
||||||
if (!ue) { host.log("no ue bridge — plugin.json needs the \"ue\" permission"); return; }
|
if (!ue) { host.log("no ue bridge — plugin.json needs the \"ue\" permission"); return; }
|
||||||
|
|
||||||
// Build the game target (UnrealBuildTool for the game module, MSBuild for plain C++).
|
// Right-click the working folder → Launch / Build, shown only on a UE project.
|
||||||
host.ui.addMenuItem("🔨 Build Solution (Unreal)", () => {
|
// Prefer the native folder-context slot (host ≥ 0.3.7); fall back to the top
|
||||||
if (!ue.hasProject()) { host.flash("No Unreal project in the current working folder.", true); return; }
|
// menu on older hosts so the actions are always reachable.
|
||||||
ue.build(); // triggers the core build flow + overlay
|
const addAction = (label, run, icon) => {
|
||||||
});
|
if (host.ui.addFolderContextItem) host.ui.addFolderContextItem(label, run, { icon, visible: () => ue.hasProject() });
|
||||||
|
else host.ui.addMenuItem(label, () => { if (!ue.hasProject()) { host.flash("No Unreal project in the current working folder.", true); return; } run(); }, { icon });
|
||||||
|
};
|
||||||
|
addAction("Launch Unreal Engine", () => ue.launch(), "ue");
|
||||||
|
addAction("Build Solution (.sln)", () => ue.build(), "hammer");
|
||||||
|
|
||||||
// Live Unreal editor log (tails Saved/Logs). Opens from the Actions menu.
|
// Bottom-dock "Unreal" tab — tails the editor log (Saved/Logs), refreshing live.
|
||||||
host.ui.addView({
|
// Native dock panel on host ≥ 0.3.7; a modal view as a fallback on older hosts.
|
||||||
id: "unreal-log",
|
const mountLog = (el) => {
|
||||||
title: "Unreal Log",
|
el.style.cssText = "display:flex;flex-direction:column;height:100%";
|
||||||
render: (el) => {
|
|
||||||
el.style.cssText = "display:flex;flex-direction:column;height:100%;min-height:340px";
|
|
||||||
const pre = document.createElement("pre");
|
const pre = document.createElement("pre");
|
||||||
pre.style.cssText =
|
pre.style.cssText =
|
||||||
"flex:1;margin:0;overflow:auto;white-space:pre-wrap;word-break:break-word;" +
|
"flex:1;margin:0;padding:10px 14px;overflow:auto;white-space:pre-wrap;word-break:break-word;" +
|
||||||
"font:11.5px/1.6 var(--mono,ui-monospace,monospace);color:var(--muted,#9a9aa8)";
|
"font:11.5px/1.6 var(--mono,ui-monospace,monospace);color:var(--muted,#9a9aa8)";
|
||||||
el.appendChild(pre);
|
el.appendChild(pre);
|
||||||
let alive = true;
|
let alive = true;
|
||||||
@ -32,15 +36,16 @@ export function activate(host) {
|
|||||||
if (!ue.hasProject()) { pre.textContent = "No Unreal project in the current working folder."; return; }
|
if (!ue.hasProject()) { pre.textContent = "No Unreal project in the current working folder."; return; }
|
||||||
const atBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 30;
|
const atBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 30;
|
||||||
const txt = await ue.readLog(200000);
|
const txt = await ue.readLog(200000);
|
||||||
pre.textContent = txt || "No Unreal log yet. It appears when the editor writes to Saved/Logs (i.e. while Unreal is running).";
|
pre.textContent = txt || "No Unreal log yet. It shows here when the editor writes to Saved/Logs — i.e. while Unreal is running.";
|
||||||
if (atBottom) pre.scrollTop = pre.scrollHeight;
|
if (atBottom) pre.scrollTop = pre.scrollHeight;
|
||||||
} catch (e) { pre.textContent = String(e); }
|
} catch (e) { pre.textContent = String(e); }
|
||||||
}
|
}
|
||||||
tick();
|
tick();
|
||||||
const id = setInterval(tick, 1500);
|
const id = setInterval(tick, 1500);
|
||||||
return () => { alive = false; clearInterval(id); };
|
return () => { alive = false; clearInterval(id); };
|
||||||
},
|
};
|
||||||
});
|
if (host.ui.addDockPanel) host.ui.addDockPanel({ id: "unreal", title: "Unreal", icon: "ue", render: mountLog });
|
||||||
|
else host.ui.addView({ id: "unreal-log", title: "Unreal Log", render: mountLog });
|
||||||
|
|
||||||
host.log("Unreal Engine Tools activated");
|
host.log("Unreal Engine Tools activated");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"id": "unreal",
|
"id": "unreal",
|
||||||
"name": "Unreal Engine Tools",
|
"name": "Unreal Engine Tools",
|
||||||
"version": "1.0.0",
|
"version": "1.2.0",
|
||||||
"author": "Exbyte Studios",
|
"author": "Exbyte Studios",
|
||||||
"description": "Build the game (.sln / UnrealBuildTool) and tail the Unreal editor log. Enable this on Unreal Engine projects.",
|
"description": "Build the game (.sln / UnrealBuildTool), launch the editor, and tail the Unreal log. Actions live in the working-folder right-click menu; the log is a bottom-dock tab. Enable on Unreal Engine projects.",
|
||||||
"entry": "index.js",
|
"entry": "index.js",
|
||||||
"minAppVersion": "0.3.6",
|
"minAppVersion": "0.3.6",
|
||||||
"permissions": ["ue"],
|
"permissions": ["ue"],
|
||||||
"defaultEnabled": true,
|
"defaultEnabled": true,
|
||||||
"contributes": { "menu": true, "views": ["unreal-log"] }
|
"contributes": { "folderContext": true, "dock": ["unreal"] }
|
||||||
}
|
}
|
||||||
|
|||||||
211
src-tauri/Cargo.lock
generated
211
src-tauri/Cargo.lock
generated
@ -468,6 +468,23 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures 0.3.0",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.45"
|
version = "0.4.45"
|
||||||
@ -558,6 +575,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -966,9 +992,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exbyte-depot"
|
name = "exbyte-depot"
|
||||||
version = "0.3.6"
|
version = "0.3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
@ -1279,8 +1306,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1302,8 +1331,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1572,6 +1604,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2042,6 +2075,12 @@ version = "0.4.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-slab"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac-notification-sys"
|
name = "mac-notification-sys"
|
||||||
version = "0.6.15"
|
version = "0.6.15"
|
||||||
@ -2784,6 +2823,62 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"lru-slab",
|
||||||
|
"rand 0.10.2",
|
||||||
|
"rand_pcg",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.46"
|
version = "1.0.46"
|
||||||
@ -2812,7 +2907,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha",
|
||||||
"rand_core",
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom 0.4.3",
|
||||||
|
"rand_core 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2822,7 +2928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2834,6 +2940,21 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_pcg"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@ -2909,6 +3030,44 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.4"
|
version = "0.13.4"
|
||||||
@ -3046,6 +3205,7 @@ version = "1.15.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3093,6 +3253,12 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@ -3315,6 +3481,18 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.21.0"
|
version = "3.21.0"
|
||||||
@ -3385,7 +3563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cpufeatures",
|
"cpufeatures 0.2.17",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3709,7 +3887,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"plist",
|
"plist",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@ -3858,7 +4036,7 @@ checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"notify-rust",
|
"notify-rust",
|
||||||
"rand",
|
"rand 0.9.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@ -3917,7 +4095,7 @@ dependencies = [
|
|||||||
"minisign-verify",
|
"minisign-verify",
|
||||||
"osakit",
|
"osakit",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"reqwest",
|
"reqwest 0.13.4",
|
||||||
"rustls",
|
"rustls",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
@ -4692,6 +4870,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web_atoms"
|
name = "web_atoms"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@ -4757,6 +4945,15 @@ dependencies = [
|
|||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.2"
|
version = "0.38.2"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "exbyte-depot"
|
name = "exbyte-depot"
|
||||||
version = "0.3.6"
|
version = "0.3.9"
|
||||||
description = "Exbyte Depot — native Perforce client by Exbyte Studios"
|
description = "Exbyte Depot — native Perforce client by Exbyte Studios"
|
||||||
authors = ["Exbyte Studios"]
|
authors = ["Exbyte Studios"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@ -26,6 +26,8 @@ serde_json = "1"
|
|||||||
tauri-plugin-dialog = "2.7.1"
|
tauri-plugin-dialog = "2.7.1"
|
||||||
# decode p4 output on non-UTF8 (ANSI/cp1251) systems — Cyrillic paths
|
# decode p4 output on non-UTF8 (ANSI/cp1251) systems — Cyrillic paths
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
|
# backend-side HTTPS for the plugin registry (webview fetch is blocked by CORS)
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||||
|
|
||||||
# Auto-updater (desktop only — checks Gitea Releases for a signed new version)
|
# Auto-updater (desktop only — checks Gitea Releases for a signed new version)
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
|||||||
@ -2987,6 +2987,25 @@ async fn plugin_write_file(id: String, name: String, content: String) -> Result<
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetch text from the Exbyte plugin registry over HTTPS on the backend — the
|
||||||
|
/// webview `fetch()` is blocked by CORS (Gitea raw sends no ACAO header), so the
|
||||||
|
/// plugin catalog and installs go through here. Restricted to the trusted host.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn registry_http_get(url: String) -> Result<String, String> {
|
||||||
|
if !url.starts_with("https://gitea.exbytestudios.com/") {
|
||||||
|
return Err("Only the Exbyte registry host is allowed".into());
|
||||||
|
}
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.user_agent("ExbyteDepot")
|
||||||
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let resp = client.get(&url).send().await.map_err(|e| e.to_string())?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(format!("HTTP {}", resp.status().as_u16()));
|
||||||
|
}
|
||||||
|
resp.text().await.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn copy_dir_recursive(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> {
|
fn copy_dir_recursive(from: &std::path::Path, to: &std::path::Path) -> std::io::Result<()> {
|
||||||
std::fs::create_dir_all(to)?;
|
std::fs::create_dir_all(to)?;
|
||||||
for e in std::fs::read_dir(from)? {
|
for e in std::fs::read_dir(from)? {
|
||||||
@ -3146,6 +3165,7 @@ pub fn run() {
|
|||||||
plugin_read_entry,
|
plugin_read_entry,
|
||||||
plugin_set_enabled,
|
plugin_set_enabled,
|
||||||
plugin_write_file,
|
plugin_write_file,
|
||||||
|
registry_http_get,
|
||||||
plugin_install,
|
plugin_install,
|
||||||
plugin_remove,
|
plugin_remove,
|
||||||
plugins_dir_path,
|
plugins_dir_path,
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Exbyte Depot",
|
"productName": "Exbyte Depot",
|
||||||
"mainBinaryName": "Exbyte Depot",
|
"mainBinaryName": "Exbyte Depot",
|
||||||
"version": "0.3.6",
|
"version": "0.3.9",
|
||||||
"identifier": "com.bonchellon.exbyte-depot",
|
"identifier": "com.bonchellon.exbyte-depot",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@ -374,6 +374,12 @@ body.resizing{cursor:col-resize!important;user-select:none}
|
|||||||
color:var(--muted);border-radius:7px;padding:3px 10px;cursor:pointer;font-size:11.5px}
|
color:var(--muted);border-radius:7px;padding:3px 10px;cursor:pointer;font-size:11.5px}
|
||||||
.stlog:hover{color:var(--txt);border-color:var(--accent)}
|
.stlog:hover{color:var(--txt);border-color:var(--accent)}
|
||||||
.stlog svg{width:13px;height:13px}
|
.stlog svg{width:13px;height:13px}
|
||||||
|
.stlog.icon{padding:3px 7px}
|
||||||
|
.stupd{position:relative}
|
||||||
|
.stupd.has{color:var(--accent);border-color:var(--accent)}
|
||||||
|
.stupd.busy{color:var(--accent-2)}
|
||||||
|
.stupd[disabled]{cursor:default;opacity:.85}
|
||||||
|
.stupd-dot{position:absolute;top:2px;right:2px;width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 2px var(--panel)}
|
||||||
.logpeek{position:absolute;left:8px;bottom:26px;width:520px;max-width:calc(100vw - 20px);max-height:min(340px,60vh);overflow-y:auto;z-index:250;
|
.logpeek{position:absolute;left:8px;bottom:26px;width:520px;max-width:calc(100vw - 20px);max-height:min(340px,60vh);overflow-y:auto;z-index:250;
|
||||||
background:var(--elevated);border:1px solid var(--border);border-radius:11px 11px 0 0;padding:5px;box-shadow:0 -14px 40px -12px rgba(0,0,0,.55)}
|
background:var(--elevated);border:1px solid var(--border);border-radius:11px 11px 0 0;padding:5px;box-shadow:0 -14px 40px -12px rgba(0,0,0,.55)}
|
||||||
.logpeek-h{position:sticky;top:0;padding:5px 9px 7px;font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--faint);
|
.logpeek-h{position:sticky;top:0;padding:5px 9px 7px;font-size:10.5px;text-transform:uppercase;letter-spacing:.5px;color:var(--faint);
|
||||||
@ -731,7 +737,8 @@ body.resizing-v{cursor:row-resize!important;user-select:none}
|
|||||||
.th-token input[type=color]::-webkit-color-swatch-wrapper{padding:0}
|
.th-token input[type=color]::-webkit-color-swatch-wrapper{padding:0}
|
||||||
/* plugins manager */
|
/* plugins manager */
|
||||||
.pl-list{flex:1;min-height:0;overflow:auto;padding:14px;display:flex;flex-direction:column;gap:10px}
|
.pl-list{flex:1;min-height:0;overflow:auto;padding:14px;display:flex;flex-direction:column;gap:10px}
|
||||||
.pl-official-h{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--faint);font-weight:700;margin:8px 2px 0;padding-top:10px;border-top:1px solid var(--border-soft)}
|
.pl-official-h{font-size:11px;text-transform:uppercase;letter-spacing:.5px;color:var(--faint);font-weight:700;margin:8px 2px 0;padding-top:10px;border-top:1px solid var(--border-soft);display:flex;align-items:baseline;gap:8px}
|
||||||
|
.pl-cat-sub{text-transform:none;letter-spacing:0;font-weight:500;color:var(--faint);opacity:.8}
|
||||||
.pl-card{display:flex;gap:12px;align-items:flex-start;border:1px solid var(--border);border-radius:12px;padding:13px 15px;background:var(--panel);transition:.15s}
|
.pl-card{display:flex;gap:12px;align-items:flex-start;border:1px solid var(--border);border-radius:12px;padding:13px 15px;background:var(--panel);transition:.15s}
|
||||||
.pl-card.off{opacity:.55}
|
.pl-card.off{opacity:.55}
|
||||||
.pl-main{flex:1;min-width:0}
|
.pl-main{flex:1;min-width:0}
|
||||||
|
|||||||
99
src/App.tsx
99
src/App.tsx
@ -7,12 +7,12 @@ import { isPermissionGranted, requestPermission, sendNotification } from "@tauri
|
|||||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||||
import ModelViewer from "./ModelViewer";
|
import ModelViewer from "./ModelViewer";
|
||||||
import CodeView from "./CodeView";
|
import CodeView from "./CodeView";
|
||||||
import UpdateBanner from "./Updater";
|
import { useUpdater } from "./Updater";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import { p4, statusOf, splitPath, kindOf, isCodeFile, fmtTime, describeFiles, filelogRevs, humanSize, leaf, saveSession, loadSession, clearSession, saveScope, loadScope, recentScopes, pushRecentScope, fileBytes, getEditor, setEditorPref, P4Info, OpenedFile, Client, Change, Session, Describe, Dir, User, Editor, FileRev, FsEntry, PluginInfo } from "./p4";
|
import { p4, statusOf, splitPath, kindOf, isCodeFile, fmtTime, describeFiles, filelogRevs, humanSize, leaf, saveSession, loadSession, clearSession, saveScope, loadScope, recentScopes, pushRecentScope, fileBytes, getEditor, setEditorPref, P4Info, OpenedFile, Client, Change, Session, Describe, Dir, User, Editor, FileRev, FsEntry, PluginInfo } from "./p4";
|
||||||
import { t, LANG, LANGS, setLangGlobal, Lang } from "./i18n";
|
import { t, LANG, LANGS, setLangGlobal, Lang } from "./i18n";
|
||||||
import { Theme, THEME_TOKENS, BUILTIN_THEMES, allThemes, loadCustomThemes, saveCustomThemes, loadActiveId, saveActiveId, resolveTheme, applyTheme, tokenValue, newCustomId, exportTheme, importTheme } from "./themes";
|
import { Theme, THEME_TOKENS, BUILTIN_THEMES, allThemes, loadCustomThemes, saveCustomThemes, loadActiveId, saveActiveId, resolveTheme, applyTheme, tokenValue, newCustomId, exportTheme, importTheme } from "./themes";
|
||||||
import { loadPlugins, getRegistry, subscribeRegistry, runSubmitHooks, PluginView, autoInstallOfficial, fetchRegistry, installFromRegistry, RegistryEntry } from "./plugins";
|
import { loadPlugins, getRegistry, subscribeRegistry, runSubmitHooks, PluginView, PluginDock, fetchRegistry, installFromRegistry, RegistryEntry } from "./plugins";
|
||||||
import { aiSummary, aiExplainCode, getExplain, saveExplain, dropExplain, initials, avatarColor, activity } from "./ai";
|
import { aiSummary, aiExplainCode, getExplain, saveExplain, dropExplain, initials, avatarColor, activity } from "./ai";
|
||||||
|
|
||||||
// native OS notification (works even when the window is hidden in the tray)
|
// native OS notification (works even when the window is hidden in the tray)
|
||||||
@ -82,6 +82,13 @@ const I = {
|
|||||||
open: <svg viewBox="0 0 24 24" fill="none"><path d="M14 4h6v6M20 4l-8 8M18 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
open: <svg viewBox="0 0 24 24" fill="none"><path d="M14 4h6v6M20 4l-8 8M18 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Resolve a plugin-declared icon NAME to a real SVG from the host set, so plugin
|
||||||
|
// contributions render with native icons instead of a generic glyph.
|
||||||
|
function pluginIcon(name?: string): ReactNode {
|
||||||
|
if (name && Object.prototype.hasOwnProperty.call(I, name)) return (I as Record<string, ReactNode>)[name];
|
||||||
|
return I.hex;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- theme hook (built-in + custom themes) ---------------- */
|
/* ---------------- theme hook (built-in + custom themes) ---------------- */
|
||||||
function useTheme(): [boolean, () => void, string, (id: string) => void] {
|
function useTheme(): [boolean, () => void, string, (id: string) => void] {
|
||||||
const [themeId, setThemeId] = useState(() => loadActiveId());
|
const [themeId, setThemeId] = useState(() => loadActiveId());
|
||||||
@ -144,7 +151,7 @@ export default function App() {
|
|||||||
onInfo={setInfo} onSession={(s) => { setSession(s); saveSession(s); }}
|
onInfo={setInfo} onSession={(s) => { setSession(s); saveSession(s); }}
|
||||||
onDisconnect={() => { p4.disconnect(); clearSession(); setInfo(null); setSession(null); setPhase("connect"); }} />;
|
onDisconnect={() => { p4.disconnect(); clearSession(); setInfo(null); setSession(null); setPhase("connect"); }} />;
|
||||||
|
|
||||||
return <>{content}<UpdateBanner /></>;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- custom styled dropdown (replaces the ugly native <select>) ---------------- */
|
/* ---------------- custom styled dropdown (replaces the ugly native <select>) ---------------- */
|
||||||
@ -413,9 +420,10 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
const [aiBusy, setAiBusy] = useState(false); // AI commit-summary generation in flight
|
const [aiBusy, setAiBusy] = useState(false); // AI commit-summary generation in flight
|
||||||
const [ctx, setCtx] = useState<null | { x: number; y: number; items: CtxItem[] }>(null); // right-click menu
|
const [ctx, setCtx] = useState<null | { x: number; y: number; items: CtxItem[] }>(null); // right-click menu
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([]); // live p4 command log (P4V-style)
|
const [logs, setLogs] = useState<LogEntry[]>([]); // live p4 command log (P4V-style)
|
||||||
// bottom dock: a tabbed panel (Log / Terminal / Unreal), opened from Window menu
|
// bottom dock: a tabbed panel (Log / Terminal + any plugin dock panels), opened from Window menu
|
||||||
const [dockOpen, setDockOpen] = useState(false);
|
const [dockOpen, setDockOpen] = useState(false);
|
||||||
const [dockTab, setDockTab] = useState<"log" | "terminal" | "unreal">("log");
|
const [dockTab, setDockTab] = useState<string>("log");
|
||||||
|
const upd = useUpdater(); // silent update check + state for the status-bar update icon
|
||||||
// right-side view panel: tabbed (File Viewer / File Tree). Header hides at one tab.
|
// right-side view panel: tabbed (File Viewer / File Tree). Header hides at one tab.
|
||||||
const [viewTabs, setViewTabs] = useState<ViewTab[]>(() => {
|
const [viewTabs, setViewTabs] = useState<ViewTab[]>(() => {
|
||||||
try { const a = JSON.parse(localStorage.getItem("exd-viewtabs") || ""); if (Array.isArray(a) && a.length && a.every((x: string) => x === "viewer" || x === "tree" || x === "locks")) return a; } catch {}
|
try { const a = JSON.parse(localStorage.getItem("exd-viewtabs") || ""); if (Array.isArray(a) && a.length && a.every((x: string) => x === "viewer" || x === "tree" || x === "locks")) return a; } catch {}
|
||||||
@ -484,7 +492,7 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
}
|
}
|
||||||
|
|
||||||
// open the bottom dock on a given tab (toggle off if already showing it)
|
// open the bottom dock on a given tab (toggle off if already showing it)
|
||||||
function openDock(tab: "log" | "terminal" | "unreal") {
|
function openDock(tab: string) {
|
||||||
setDockOpen((o) => (o && dockTab === tab ? false : true));
|
setDockOpen((o) => (o && dockTab === tab ? false : true));
|
||||||
setDockTab(tab);
|
setDockTab(tab);
|
||||||
}
|
}
|
||||||
@ -629,6 +637,7 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
hasProject: () => !!uprojectRef.current,
|
hasProject: () => !!uprojectRef.current,
|
||||||
uproject: () => uprojectRef.current,
|
uproject: () => uprojectRef.current,
|
||||||
build: () => { startBuild(); },
|
build: () => { startBuild(); },
|
||||||
|
launch: () => { void launchUE(); },
|
||||||
readLog: (tb?: number) => p4.ueLog(uprojectRef.current, tb ?? 200_000),
|
readLog: (tb?: number) => p4.ueLog(uprojectRef.current, tb ?? 200_000),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -640,12 +649,8 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
if (pluginsLoaded.current || !info?.clientName) return;
|
if (pluginsLoaded.current || !info?.clientName) return;
|
||||||
pluginsLoaded.current = true;
|
pluginsLoaded.current = true;
|
||||||
(async () => {
|
(async () => {
|
||||||
// auto-install official plugins (e.g. Unreal) on first run so no one loses tooling
|
// No auto-install: only load what the user chose to install. New plugins are
|
||||||
try {
|
// browsed and installed by hand from Settings → Plugins (the registry catalog).
|
||||||
const installed = new Set((await p4.pluginList()).map((p) => p.id));
|
|
||||||
const added = await autoInstallOfficial(installed);
|
|
||||||
if (added.length) console.log("[plugins] auto-installed:", added);
|
|
||||||
} catch { /* offline — skip */ }
|
|
||||||
const { loaded, errors } = await reloadPlugins();
|
const { loaded, errors } = await reloadPlugins();
|
||||||
if (errors.length) console.warn("[plugins] load errors:", errors);
|
if (errors.length) console.warn("[plugins] load errors:", errors);
|
||||||
if (loaded) flash(t("{n} plugin(s) loaded", { n: loaded }));
|
if (loaded) flash(t("{n} plugin(s) loaded", { n: loaded }));
|
||||||
@ -1286,7 +1291,7 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
...(isCode ? [{ label: t("Blame (annotate)"), icon: I.people, act: () => setModal({ kind: "blame", spec: dp, name: splitPath(dp).name }) }] : []),
|
...(isCode ? [{ label: t("Blame (annotate)"), icon: I.people, act: () => setModal({ kind: "blame", spec: dp, name: splitPath(dp).name }) }] : []),
|
||||||
{ label: t("Open in Explorer"), icon: I.reveal, act: () => reveal(dp) },
|
{ label: t("Open in Explorer"), icon: I.reveal, act: () => reveal(dp) },
|
||||||
{ label: t("Copy path"), icon: I.copy, act: () => copyText(dp, t("Path")) },
|
{ label: t("Copy path"), icon: I.copy, act: () => copyText(dp, t("Path")) },
|
||||||
...getRegistry().fileItems.map((fi) => ({ label: fi.label, icon: I.hex, act: () => { void Promise.resolve(fi.run(view[i] as { depotFile?: string })).catch((err) => flash(String(err), true)); } })),
|
...getRegistry().fileItems.map((fi) => ({ label: fi.label, icon: pluginIcon(fi.icon), act: () => { void Promise.resolve(fi.run(view[i] as { depotFile?: string })).catch((err) => flash(String(err), true)); } })),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
async function lockFiles(dps: string[], lock: boolean) {
|
async function lockFiles(dps: string[], lock: boolean) {
|
||||||
@ -1397,6 +1402,7 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
{ label: (viewTabs.includes("locks") ? "✓ " : "") + t("File Locks"), icon: I.lock, hint: t("Everyone's checked-out / exclusively-locked files, docked into the view panel."), act: () => toggleViewTab("locks") },
|
{ label: (viewTabs.includes("locks") ? "✓ " : "") + t("File Locks"), icon: I.lock, hint: t("Everyone's checked-out / exclusively-locked files, docked into the view panel."), act: () => toggleViewTab("locks") },
|
||||||
{ label: (dockOpen && dockTab === "log" ? "✓ " : "") + t("Log"), kb: "Ctrl+L", icon: I.log, hint: t("Show the panel of recently run Perforce commands."), act: () => openDock("log") },
|
{ label: (dockOpen && dockTab === "log" ? "✓ " : "") + t("Log"), kb: "Ctrl+L", icon: I.log, hint: t("Show the panel of recently run Perforce commands."), act: () => openDock("log") },
|
||||||
{ label: (dockOpen && dockTab === "terminal" ? "✓ " : "") + t("Terminal"), kb: "Ctrl+`", icon: I.terminal, hint: t("Open an embedded terminal for raw p4 commands."), act: () => openDock("terminal") },
|
{ label: (dockOpen && dockTab === "terminal" ? "✓ " : "") + t("Terminal"), kb: "Ctrl+`", icon: I.terminal, hint: t("Open an embedded terminal for raw p4 commands."), act: () => openDock("terminal") },
|
||||||
|
...getRegistry().docks.map((d) => ({ label: (dockOpen && dockTab === `plugin:${d.id}` ? "✓ " : "") + d.title, icon: pluginIcon(d.icon), hint: t("From plugin: {p}", { p: d.pluginId }), act: () => openDock(`plugin:${d.id}`) })),
|
||||||
],
|
],
|
||||||
Tools: [
|
Tools: [
|
||||||
{ label: t("Search depot…"), icon: I.search, hint: t("Find files anywhere in the depot by name or path."), act: () => setModal({ kind: "search" }) },
|
{ label: t("Search depot…"), icon: I.search, hint: t("Find files anywhere in the depot by name or path."), act: () => setModal({ kind: "search" }) },
|
||||||
@ -1407,7 +1413,7 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
{ label: t("Streams…"), icon: I.branch, hint: t("Switch stream, merge down from the parent, or copy up to it."), act: () => setModal({ kind: "streams" }) },
|
{ label: t("Streams…"), icon: I.branch, hint: t("Switch stream, merge down from the parent, or copy up to it."), act: () => setModal({ kind: "streams" }) },
|
||||||
{ label: t("Jobs…"), icon: I.log, hint: t("Perforce's task/bug tracker — create jobs and attach them to changelists."), act: () => setModal({ kind: "jobs" }) },
|
{ label: t("Jobs…"), icon: I.log, hint: t("Perforce's task/bug tracker — create jobs and attach them to changelists."), act: () => setModal({ kind: "jobs" }) },
|
||||||
{ label: t("Edit .p4ignore…"), icon: I.gear, hint: t("Edit which files reconcile / add ignore (build output, caches)."), act: () => setModal({ kind: "ignore" }) },
|
{ label: t("Edit .p4ignore…"), icon: I.gear, hint: t("Edit which files reconcile / add ignore (build output, caches)."), act: () => setModal({ kind: "ignore" }) },
|
||||||
...getRegistry().menu.map((m) => ({ label: m.label, icon: I.hex, hint: t("From plugin: {p}", { p: m.pluginId }), act: () => { void Promise.resolve(m.run()).catch((e) => flash(String(e), true)); } })),
|
...getRegistry().menu.map((m) => ({ label: m.label, icon: pluginIcon(m.icon), hint: t("From plugin: {p}", { p: m.pluginId }), act: () => { void Promise.resolve(m.run()).catch((e) => flash(String(e), true)); } })),
|
||||||
...getRegistry().views.map((v) => ({ label: v.title, icon: I.hex, hint: t("Plugin view"), act: () => setPluginView(v) })),
|
...getRegistry().views.map((v) => ({ label: v.title, icon: I.hex, hint: t("Plugin view"), act: () => setPluginView(v) })),
|
||||||
{ label: t("People & Roles…"), icon: I.people, hint: t("See who works on this depot and their roles."), act: () => setModal({ kind: "users" }) },
|
{ label: t("People & Roles…"), icon: I.people, hint: t("See who works on this depot and their roles."), act: () => setModal({ kind: "users" }) },
|
||||||
{ label: t("Settings…"), icon: I.gear, hint: t("App preferences — language, theme, editor, and more."), act: () => setModal({ kind: "settings" }) },
|
{ label: t("Settings…"), icon: I.gear, hint: t("App preferences — language, theme, editor, and more."), act: () => setModal({ kind: "settings" }) },
|
||||||
@ -1460,7 +1466,7 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
...(activePath ? [{ label: t("Rename…"), icon: I.pencil, act: () => renameFolder(activePath) }] : []),
|
...(activePath ? [{ label: t("Rename…"), icon: I.pencil, act: () => renameFolder(activePath) }] : []),
|
||||||
...(activePath ? [{ label: t("Properties"), icon: I.info, act: () => setModal({ kind: "folderprops", path: activePath, name: splitPath(activePath).name || activePath }) }] : []),
|
...(activePath ? [{ label: t("Properties"), icon: I.info, act: () => setModal({ kind: "folderprops", path: activePath, name: splitPath(activePath).name || activePath }) }] : []),
|
||||||
{ label: t("Show in File Tree"), icon: I.folder, act: () => showInTree("depot") },
|
{ label: t("Show in File Tree"), icon: I.folder, act: () => showInTree("depot") },
|
||||||
...(uproject ? [{ label: t("Launch Unreal Engine"), icon: I.ue, act: launchUE }] : []),
|
...getRegistry().folderItems.filter((f) => !f.visible || f.visible()).map((f) => ({ label: f.label, icon: pluginIcon(f.icon), act: () => { void Promise.resolve(f.run()).catch((e) => flash(String(e), true)); } })),
|
||||||
{ label: t("Open in Explorer"), icon: I.reveal, act: () => reveal(activePath || String(info?.clientRoot || "")) },
|
{ label: t("Open in Explorer"), icon: I.reveal, act: () => reveal(activePath || String(info?.clientRoot || "")) },
|
||||||
{ label: t("Choose another folder…"), icon: I.folder, act: () => browseTo("") },
|
{ label: t("Choose another folder…"), icon: I.folder, act: () => browseTo("") },
|
||||||
{ label: t("Copy path"), icon: I.copy, act: () => copyText(activePath || "//…", t("Path")) },
|
{ label: t("Copy path"), icon: I.copy, act: () => copyText(activePath || "//…", t("Path")) },
|
||||||
@ -1693,7 +1699,7 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dockOpen && <DockPanel tab={dockTab} setTab={setDockTab} onClose={() => setDockOpen(false)}
|
{dockOpen && <DockPanel tab={dockTab} setTab={setDockTab} onClose={() => setDockOpen(false)}
|
||||||
height={dockH} onResize={startDockResize}
|
height={dockH} onResize={startDockResize} docks={getRegistry().docks}
|
||||||
logs={logs} onClearLogs={() => setLogs([])}
|
logs={logs} onClearLogs={() => setLogs([])}
|
||||||
termLines={termLines} termInput={termInput} setTermInput={setTermInput} termBusy={termBusy}
|
termLines={termLines} termInput={termInput} setTermInput={setTermInput} termBusy={termBusy}
|
||||||
onRun={runConsole} cmdHist={cmdHist} onClearTerm={() => setTermLines([])} />}
|
onRun={runConsole} cmdHist={cmdHist} onClearTerm={() => setTermLines([])} />}
|
||||||
@ -1706,9 +1712,20 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
|||||||
<span className="stspace" />
|
<span className="stspace" />
|
||||||
<button className="stlog" onClick={() => openDock("terminal")} title="Ctrl+`">{I.terminal}<span>{t("Terminal")}</span></button>
|
<button className="stlog" onClick={() => openDock("terminal")} title="Ctrl+`">{I.terminal}<span>{t("Terminal")}</span></button>
|
||||||
<button className="stlog" onClick={() => openDock("log")} title="Ctrl+L">{I.log}<span>Log</span></button>
|
<button className="stlog" onClick={() => openDock("log")} title="Ctrl+L">{I.log}<span>Log</span></button>
|
||||||
<button className="stlog" onClick={() => window.dispatchEvent(new Event("exd-check-update"))} title={t("Check for updates")}>
|
<button
|
||||||
<svg viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
className={"stlog stupd icon" + (upd.phase === "available" ? " has" : "") + (upd.phase === "downloading" || upd.phase === "checking" ? " busy" : "")}
|
||||||
<span>{t("Updates")}</span>
|
onClick={() => (upd.phase === "available" ? upd.install() : upd.check())}
|
||||||
|
disabled={upd.phase === "downloading"}
|
||||||
|
title={
|
||||||
|
upd.phase === "available" ? t("Update available — click to install")
|
||||||
|
: upd.phase === "downloading" ? t("Downloading… {pct}%", { pct: upd.pct })
|
||||||
|
: upd.phase === "done" ? t("Installed — restarting…")
|
||||||
|
: t("Check for updates")
|
||||||
|
}>
|
||||||
|
{upd.phase === "checking" || upd.phase === "downloading"
|
||||||
|
? <span className="ldr sm" />
|
||||||
|
: <svg viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" /></svg>}
|
||||||
|
{upd.phase === "available" && <span className="stupd-dot" />}
|
||||||
</button>
|
</button>
|
||||||
{peek && logs.length > 0 && (
|
{peek && logs.length > 0 && (
|
||||||
<div className="logpeek" onMouseEnter={showPeek} onMouseLeave={hidePeek}>
|
<div className="logpeek" onMouseEnter={showPeek} onMouseLeave={hidePeek}>
|
||||||
@ -2059,18 +2076,20 @@ function LogRow({ l }: { l: LogEntry }) {
|
|||||||
// common p4 subcommands for the terminal's autocomplete
|
// common p4 subcommands for the terminal's autocomplete
|
||||||
const P4_COMMANDS = ["add","annotate","branch","branches","change","changes","client","clients","counter","counters","delete","depot","depots","describe","diff","dirs","edit","filelog","files","fstat","group","groups","have","info","integrate","labels","login","logout","monitor","move","opened","print","protect","reconcile","reopen","resolve","resolved","revert","reviews","shelve","sizes","status","stream","streams","submit","sync","tag","tickets","unshelve","user","users","where"];
|
const P4_COMMANDS = ["add","annotate","branch","branches","change","changes","client","clients","counter","counters","delete","depot","depots","describe","diff","dirs","edit","filelog","files","fstat","group","groups","have","info","integrate","labels","login","logout","monitor","move","opened","print","protect","reconcile","reopen","resolve","resolved","revert","reviews","shelve","sizes","status","stream","streams","submit","sync","tag","tickets","unshelve","user","users","where"];
|
||||||
|
|
||||||
/* ---------------- bottom dock: tabbed Log / Terminal / Unreal ---------------- */
|
/* ---------------- bottom dock: tabbed Log / Terminal + plugin dock panels ---------------- */
|
||||||
function DockPanel({ tab, setTab, onClose, height, onResize, logs, onClearLogs, termLines, termInput, setTermInput, termBusy, onRun, cmdHist, onClearTerm }: {
|
function DockPanel({ tab, setTab, onClose, height, onResize, docks, logs, onClearLogs, termLines, termInput, setTermInput, termBusy, onRun, cmdHist, onClearTerm }: {
|
||||||
tab: "log" | "terminal" | "unreal"; setTab: (t: "log" | "terminal" | "unreal") => void; onClose: () => void;
|
tab: string; setTab: (t: string) => void; onClose: () => void;
|
||||||
height: number; onResize: (e: ReactMouseEvent) => void;
|
height: number; onResize: (e: ReactMouseEvent) => void; docks: PluginDock[];
|
||||||
logs: LogEntry[]; onClearLogs: () => void;
|
logs: LogEntry[]; onClearLogs: () => void;
|
||||||
termLines: TermLine[]; termInput: string; setTermInput: (s: string) => void; termBusy: boolean;
|
termLines: TermLine[]; termInput: string; setTermInput: (s: string) => void; termBusy: boolean;
|
||||||
onRun: (cmd: string) => void; cmdHist: string[]; onClearTerm: () => void;
|
onRun: (cmd: string) => void; cmdHist: string[]; onClearTerm: () => void;
|
||||||
}) {
|
}) {
|
||||||
const tabs: { key: "log" | "terminal" | "unreal"; label: string; icon: ReactNode }[] = [
|
const tabs: { key: string; label: string; icon: ReactNode }[] = [
|
||||||
{ key: "log", label: t("Log"), icon: I.log },
|
{ key: "log", label: t("Log"), icon: I.log },
|
||||||
{ key: "terminal", label: t("Terminal"), icon: I.terminal },
|
{ key: "terminal", label: t("Terminal"), icon: I.terminal },
|
||||||
|
...docks.map((d) => ({ key: `plugin:${d.id}`, label: d.title, icon: pluginIcon(d.icon) })),
|
||||||
];
|
];
|
||||||
|
const activeDock = tab.startsWith("plugin:") ? docks.find((d) => `plugin:${d.id}` === tab) : undefined;
|
||||||
return (
|
return (
|
||||||
<div className="dockpanel" style={{ flexBasis: height, height }}>
|
<div className="dockpanel" style={{ flexBasis: height, height }}>
|
||||||
<div className="dockhandle" onMouseDown={onResize} title={t("Drag to resize")} />
|
<div className="dockhandle" onMouseDown={onResize} title={t("Drag to resize")} />
|
||||||
@ -2090,11 +2109,26 @@ function DockPanel({ tab, setTab, onClose, height, onResize, logs, onClearLogs,
|
|||||||
<div className="dockpane" key={tab}>
|
<div className="dockpane" key={tab}>
|
||||||
{tab === "log" && <LogTab logs={logs} />}
|
{tab === "log" && <LogTab logs={logs} />}
|
||||||
{tab === "terminal" && <TerminalTab lines={termLines} input={termInput} setInput={setTermInput} busy={termBusy} onRun={onRun} cmdHist={cmdHist} />}
|
{tab === "terminal" && <TerminalTab lines={termLines} input={termInput} setInput={setTermInput} busy={termBusy} onRun={onRun} cmdHist={cmdHist} />}
|
||||||
|
{activeDock && <PluginDockPanel dock={activeDock} />}
|
||||||
|
{tab.startsWith("plugin:") && !activeDock && <div className="logempty">{t("This panel's plugin is no longer active.")}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host for a plugin-provided dock panel: mounts the plugin's render() into a div
|
||||||
|
// and runs its cleanup on unmount / tab switch. Same pattern as PluginViewModal.
|
||||||
|
function PluginDockPanel({ dock }: { dock: PluginDock }) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current; if (!el) return;
|
||||||
|
let cleanup: void | (() => void);
|
||||||
|
try { cleanup = dock.render(el); } catch (e) { el.textContent = String(e); }
|
||||||
|
return () => { try { (cleanup as (() => void) | undefined)?.(); } catch { /* ignore */ } el.innerHTML = ""; };
|
||||||
|
}, [dock]);
|
||||||
|
return <div className="dockplugin" ref={ref} style={{ height: "100%", overflow: "auto" }} />;
|
||||||
|
}
|
||||||
|
|
||||||
function LogTab({ logs }: { logs: LogEntry[] }) {
|
function LogTab({ logs }: { logs: LogEntry[] }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
useEffect(() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [logs]);
|
useEffect(() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [logs]);
|
||||||
@ -3249,9 +3283,12 @@ function PluginsModal({ onClose, onReload, onFlash, embedded }: { onClose?: () =
|
|||||||
const [busy, setBusy] = useState(true);
|
const [busy, setBusy] = useState(true);
|
||||||
const [dir, setDir] = useState("");
|
const [dir, setDir] = useState("");
|
||||||
const [reg, setReg] = useState<RegistryEntry[]>([]);
|
const [reg, setReg] = useState<RegistryEntry[]>([]);
|
||||||
|
const [regBusy, setRegBusy] = useState(true);
|
||||||
|
const [regErr, setRegErr] = useState("");
|
||||||
const [installing, setInstalling] = useState<string>("");
|
const [installing, setInstalling] = useState<string>("");
|
||||||
const load = () => { setBusy(true); p4.pluginList().then(setList).catch((e) => onFlash(String(e), true)).finally(() => setBusy(false)); };
|
const load = () => { setBusy(true); p4.pluginList().then(setList).catch((e) => onFlash(String(e), true)).finally(() => setBusy(false)); };
|
||||||
useEffect(() => { load(); p4.pluginsDirPath().then(setDir).catch(() => {}); fetchRegistry().then(setReg).catch(() => {}); const k = (e: KeyboardEvent) => e.key === "Escape" && onClose?.(); document.addEventListener("keydown", k); return () => document.removeEventListener("keydown", k); }, []); // eslint-disable-line
|
const loadReg = () => { setRegBusy(true); setRegErr(""); fetchRegistry().then(setReg).catch((e) => setRegErr(String(e))).finally(() => setRegBusy(false)); };
|
||||||
|
useEffect(() => { load(); loadReg(); p4.pluginsDirPath().then(setDir).catch(() => {}); const k = (e: KeyboardEvent) => e.key === "Escape" && onClose?.(); document.addEventListener("keydown", k); return () => document.removeEventListener("keydown", k); }, []); // eslint-disable-line
|
||||||
async function installReg(entry: RegistryEntry) {
|
async function installReg(entry: RegistryEntry) {
|
||||||
setInstalling(entry.id);
|
setInstalling(entry.id);
|
||||||
try { await installFromRegistry(entry); load(); await onReload(); onFlash(t("Installed plugin “{id}”.", { id: entry.name })); }
|
try { await installFromRegistry(entry); load(); await onReload(); onFlash(t("Installed plugin “{id}”.", { id: entry.name })); }
|
||||||
@ -3274,7 +3311,7 @@ function PluginsModal({ onClose, onReload, onFlash, embedded }: { onClose?: () =
|
|||||||
} catch (e) { onFlash(String(e), true); }
|
} catch (e) { onFlash(String(e), true); }
|
||||||
}
|
}
|
||||||
const body = (<>
|
const body = (<>
|
||||||
<div className="tm-hint">{t("Plugins add features on top of the core app. Install a folder with a plugin.json, toggle plugins on/off, then they load automatically. Plugins run in-app — only install ones you trust.")}</div>
|
<div className="tm-hint">{t("Plugins add features on top of the core app. Browse the catalog below and install what you need — nothing is installed automatically. Toggle plugins on/off; enabled ones load on launch. Plugins run in-app — only install ones you trust.")}</div>
|
||||||
<div className="pl-list">
|
<div className="pl-list">
|
||||||
{busy ? <div className="ur-empty"><span className="ldr" /> {t("Loading…")}</div>
|
{busy ? <div className="ur-empty"><span className="ldr" /> {t("Loading…")}</div>
|
||||||
: list.length === 0 ? <div className="ur-empty">{I.hex}{t("No plugins installed yet.\nInstall one from a folder, or drop it into the plugins folder below.")}</div>
|
: list.length === 0 ? <div className="ur-empty">{I.hex}{t("No plugins installed yet.\nInstall one from a folder, or drop it into the plugins folder below.")}</div>
|
||||||
@ -3294,9 +3331,12 @@ function PluginsModal({ onClose, onReload, onFlash, embedded }: { onClose?: () =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{reg.filter((e) => !list.some((p) => p.id === e.id)).length > 0 && (<>
|
<div className="pl-official-h">{t("Available to install")}<span className="pl-cat-sub">{t("from the Exbyte plugin registry")}</span></div>
|
||||||
<div className="pl-official-h">{t("Official plugins")}</div>
|
{regBusy ? <div className="ur-empty"><span className="ldr" /> {t("Loading the plugin catalog…")}</div>
|
||||||
{reg.filter((e) => !list.some((p) => p.id === e.id)).map((e) => (
|
: regErr ? <div className="ur-empty">{I.hex}{t("Couldn't reach the plugin registry.")}<button className="mbtn ghost" style={{ flex: "none", padding: "6px 12px", marginTop: 8 }} onClick={loadReg}>{I.sync}{t("Retry")}</button></div>
|
||||||
|
: reg.filter((e) => !list.some((p) => p.id === e.id)).length === 0
|
||||||
|
? <div className="ur-empty">{I.check}{t("Everything from the catalog is already installed.")}</div>
|
||||||
|
: reg.filter((e) => !list.some((p) => p.id === e.id)).map((e) => (
|
||||||
<div key={e.id} className="pl-card">
|
<div key={e.id} className="pl-card">
|
||||||
<div className="pl-main">
|
<div className="pl-main">
|
||||||
<div className="pl-top"><b>{e.name}</b><span className="pl-ver">v{e.version}</span></div>
|
<div className="pl-top"><b>{e.name}</b><span className="pl-ver">v{e.version}</span></div>
|
||||||
@ -3308,7 +3348,6 @@ function PluginsModal({ onClose, onReload, onFlash, embedded }: { onClose?: () =
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</>)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-actions" style={{ padding: "12px 16px", borderTop: "1px solid var(--border-soft)" }}>
|
<div className="modal-actions" style={{ padding: "12px 16px", borderTop: "1px solid var(--border-soft)" }}>
|
||||||
<button className="mbtn ghost" onClick={() => p4.openPluginsDir().catch(() => {})} title={dir}>{I.reveal}{t("Open plugins folder")}</button>
|
<button className="mbtn ghost" onClick={() => p4.openPluginsDir().catch(() => {})} title={dir}>{I.reveal}{t("Open plugins folder")}</button>
|
||||||
|
|||||||
120
src/Updater.tsx
120
src/Updater.tsx
@ -1,53 +1,44 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { check, type Update } from "@tauri-apps/plugin-updater";
|
import { check, type Update } from "@tauri-apps/plugin-updater";
|
||||||
import { relaunch } from "@tauri-apps/plugin-process";
|
import { relaunch } from "@tauri-apps/plugin-process";
|
||||||
import { t } from "./i18n";
|
|
||||||
|
|
||||||
type Phase = "idle" | "checking" | "available" | "uptodate" | "downloading" | "done" | "error";
|
export type UpdPhase = "idle" | "checking" | "available" | "uptodate" | "downloading" | "done" | "error";
|
||||||
|
|
||||||
// Update prompt. Quietly checks the Gitea Releases endpoint a few seconds after
|
export interface UpdaterState {
|
||||||
// launch, and again on demand when the "Updates" button fires `exd-check-update`.
|
phase: UpdPhase;
|
||||||
// If a newer signed version exists, slides a small card into the bottom-right.
|
pct: number;
|
||||||
export default function UpdateBanner() {
|
err: string;
|
||||||
|
version?: string;
|
||||||
|
check: () => void; // manual check
|
||||||
|
install: () => void; // download + install the pending update
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update logic as a hook. Silently checks the Gitea Releases endpoint a few
|
||||||
|
// seconds after mount, and again on demand (manual). The UI is just a single
|
||||||
|
// download icon in the status bar (see Workbench) — no floating card.
|
||||||
|
export function useUpdater(): UpdaterState {
|
||||||
const [update, setUpdate] = useState<Update | null>(null);
|
const [update, setUpdate] = useState<Update | null>(null);
|
||||||
const [phase, setPhase] = useState<Phase>("idle");
|
const [phase, setPhase] = useState<UpdPhase>("idle");
|
||||||
const [pct, setPct] = useState(0);
|
const [pct, setPct] = useState(0);
|
||||||
const [err, setErr] = useState("");
|
const [err, setErr] = useState("");
|
||||||
const [hidden, setHidden] = useState(false);
|
|
||||||
const busyRef = useRef(false);
|
const busyRef = useRef(false);
|
||||||
|
|
||||||
// shared check routine. `manual` shows "checking" / "up to date" feedback.
|
|
||||||
async function runCheck(manual: boolean) {
|
async function runCheck(manual: boolean) {
|
||||||
if (busyRef.current || phase === "downloading") return;
|
if (busyRef.current || phase === "downloading") return;
|
||||||
busyRef.current = true;
|
busyRef.current = true;
|
||||||
if (manual) { setHidden(false); setPhase("checking"); }
|
if (manual) setPhase("checking");
|
||||||
try {
|
try {
|
||||||
const u = await check();
|
const u = await check();
|
||||||
if (u) { setUpdate(u); setPhase("available"); setHidden(false); }
|
if (u) { setUpdate(u); setPhase("available"); }
|
||||||
else if (manual) { setPhase("uptodate"); setHidden(false); }
|
else { setPhase(manual ? "uptodate" : "idle"); }
|
||||||
else setPhase("idle");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (manual) { setErr(String(e)); setPhase("error"); setHidden(false); }
|
setErr(String(e));
|
||||||
|
if (manual) setPhase("error");
|
||||||
} finally { busyRef.current = false; }
|
} finally { busyRef.current = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial silent check + listen for manual "Updates" clicks
|
|
||||||
useEffect(() => {
|
|
||||||
const id = setTimeout(() => runCheck(false), 4000);
|
|
||||||
const onManual = () => runCheck(true);
|
|
||||||
window.addEventListener("exd-check-update", onManual);
|
|
||||||
return () => { clearTimeout(id); window.removeEventListener("exd-check-update", onManual); };
|
|
||||||
}, []); // eslint-disable-line
|
|
||||||
|
|
||||||
// auto-dismiss the transient "up to date" card
|
|
||||||
useEffect(() => {
|
|
||||||
if (phase !== "uptodate") return;
|
|
||||||
const id = setTimeout(() => { setHidden(true); setPhase("idle"); }, 3500);
|
|
||||||
return () => clearTimeout(id);
|
|
||||||
}, [phase]);
|
|
||||||
|
|
||||||
async function install() {
|
async function install() {
|
||||||
if (!update) return;
|
if (!update) { void runCheck(true); return; }
|
||||||
setPhase("downloading"); setPct(0);
|
setPhase("downloading"); setPct(0);
|
||||||
let total = 0, got = 0;
|
let total = 0, got = 0;
|
||||||
try {
|
try {
|
||||||
@ -61,59 +52,20 @@ export default function UpdateBanner() {
|
|||||||
} catch (e) { setErr(String(e)); setPhase("error"); }
|
} catch (e) { setErr(String(e)); setPhase("error"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hidden || phase === "idle") return null;
|
// initial silent check + listen for manual "Updates" clicks (kept for compatibility)
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => runCheck(false), 4000);
|
||||||
|
const onManual = () => runCheck(true);
|
||||||
|
window.addEventListener("exd-check-update", onManual);
|
||||||
|
return () => { clearTimeout(id); window.removeEventListener("exd-check-update", onManual); };
|
||||||
|
}, []); // eslint-disable-line
|
||||||
|
|
||||||
// compact cards for the transient states (no `update` object yet)
|
// let the transient "checked, up to date" / "error" state fade back to idle
|
||||||
if (phase === "checking") {
|
useEffect(() => {
|
||||||
return <div className="upd sm"><div className="upd-head"><span className="upd-ic"><span className="ldr sm" /></span><div className="upd-ttl"><b>{t("Checking for updates…")}</b></div></div></div>;
|
if (phase !== "uptodate" && phase !== "error") return;
|
||||||
}
|
const id = setTimeout(() => setPhase("idle"), 3500);
|
||||||
if (phase === "uptodate") {
|
return () => clearTimeout(id);
|
||||||
return <div className="upd sm ok" onClick={() => { setHidden(true); setPhase("idle"); }}>
|
}, [phase]);
|
||||||
<div className="upd-head"><span className="upd-ic ok"><svg viewBox="0 0 24 24" fill="none"><path d="M5 12l5 5 9-11" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" /></svg></span>
|
|
||||||
<div className="upd-ttl"><b>{t("You're on the latest version")}</b></div></div>
|
return { phase, pct, err, version: update?.version, check: () => runCheck(true), install };
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
if (phase === "error") {
|
|
||||||
return <div className="upd sm"><div className="upd-head"><span className="upd-ic"><svg viewBox="0 0 24 24" fill="none"><path d="M12 8v5m0 3h.01M10.3 4l-7 12A1.5 1.5 0 0 0 4.6 18h14.8a1.5 1.5 0 0 0 1.3-2L13.7 4a1.5 1.5 0 0 0-3.4 0Z" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" /></svg></span>
|
|
||||||
<div className="upd-ttl"><b>{t("Update check failed")}</b><span>{err.slice(0, 80)}</span></div>
|
|
||||||
<button className="upd-x" onClick={() => { setHidden(true); setPhase("idle"); }}><svg viewBox="0 0 24 24" width="13" height="13" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg></button></div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
if (!update) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="upd">
|
|
||||||
<div className="upd-head">
|
|
||||||
<span className="upd-ic">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" /></svg>
|
|
||||||
</span>
|
|
||||||
<div className="upd-ttl">
|
|
||||||
<b>{t("Update available")}</b>
|
|
||||||
<span>Exbyte Depot v{update.version}</span>
|
|
||||||
</div>
|
|
||||||
{phase === "available" && (
|
|
||||||
<button className="upd-x" onClick={() => setHidden(true)} title={t("Later")}>
|
|
||||||
<svg viewBox="0 0 24 24" width="13" height="13" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{update.body?.trim() && phase === "available" && <div className="upd-notes">{update.body.trim()}</div>}
|
|
||||||
|
|
||||||
{phase === "downloading" && (
|
|
||||||
<div className="upd-prog">
|
|
||||||
<div className="upd-track"><div className="upd-fill" style={{ width: `${pct}%` }} /></div>
|
|
||||||
<span>{t("Downloading… {pct}%", { pct })}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{phase === "done" && <div className="upd-notes">{t("Installed — restarting…")}</div>}
|
|
||||||
|
|
||||||
{phase === "available" && (
|
|
||||||
<div className="upd-actions">
|
|
||||||
<button className="upd-later" onClick={() => setHidden(true)}>{t("Later")}</button>
|
|
||||||
<button className="upd-go" onClick={install}>{t("Update now")}</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -180,6 +180,7 @@ const D: Record<string, Tr> = {
|
|||||||
"Plugins": { ru: "Плагины", de: "Plugins", fr: "Extensions", es: "Plugins" },
|
"Plugins": { ru: "Плагины", de: "Plugins", fr: "Extensions", es: "Plugins" },
|
||||||
"General": { ru: "Общие", de: "Allgemein", fr: "Général", es: "General" },
|
"General": { ru: "Общие", de: "Allgemein", fr: "Général", es: "General" },
|
||||||
"Check for updates": { ru: "Проверить обновления", de: "Nach Updates suchen", fr: "Vérifier les mises à jour", es: "Buscar actualizaciones" },
|
"Check for updates": { ru: "Проверить обновления", de: "Nach Updates suchen", fr: "Vérifier les mises à jour", es: "Buscar actualizaciones" },
|
||||||
|
"Update available — click to install": { ru: "Есть обновление — нажми, чтобы установить", de: "Update verfügbar — zum Installieren klicken", fr: "Mise à jour dispo — cliquez pour installer", es: "Actualización disponible — clic para instalar" },
|
||||||
"Updates": { ru: "Обновления", de: "Updates", fr: "Mises à jour", es: "Actualizaciones" },
|
"Updates": { ru: "Обновления", de: "Updates", fr: "Mises à jour", es: "Actualizaciones" },
|
||||||
"Checking for updates…": { ru: "Проверяю обновления…", de: "Suche nach Updates…", fr: "Recherche de mises à jour…", es: "Buscando actualizaciones…" },
|
"Checking for updates…": { ru: "Проверяю обновления…", de: "Suche nach Updates…", fr: "Recherche de mises à jour…", es: "Buscando actualizaciones…" },
|
||||||
"You're on the latest version": { ru: "У тебя последняя версия", de: "Du hast die neueste Version", fr: "Vous avez la dernière version", es: "Tienes la última versión" },
|
"You're on the latest version": { ru: "У тебя последняя версия", de: "Du hast die neueste Version", fr: "Vous avez la dernière version", es: "Tienes la última versión" },
|
||||||
@ -187,11 +188,17 @@ const D: Record<string, Tr> = {
|
|||||||
"Appearance": { ru: "Оформление", de: "Darstellung", fr: "Apparence", es: "Apariencia" },
|
"Appearance": { ru: "Оформление", de: "Darstellung", fr: "Apparence", es: "Apariencia" },
|
||||||
"Base theme": { ru: "Базовая тема", de: "Basis-Theme", fr: "Thème de base", es: "Tema base" },
|
"Base theme": { ru: "Базовая тема", de: "Basis-Theme", fr: "Thème de base", es: "Tema base" },
|
||||||
"{n} installed": { ru: "установлено: {n}", de: "{n} installiert", fr: "{n} installés", es: "{n} instalados" },
|
"{n} installed": { ru: "установлено: {n}", de: "{n} installiert", fr: "{n} installés", es: "{n} instalados" },
|
||||||
"Plugins add features on top of the core app. Install a folder with a plugin.json, toggle plugins on/off, then they load automatically. Plugins run in-app — only install ones you trust.": { ru: "Плагины добавляют функции поверх ядра. Установи папку с plugin.json, включай/выключай — они грузятся автоматически. Плагины работают внутри приложения — ставь только те, которым доверяешь.", de: "Plugins fügen Funktionen zur Kern-App hinzu. Installiere einen Ordner mit plugin.json, schalte Plugins ein/aus — sie laden automatisch. Plugins laufen in der App — installiere nur vertrauenswürdige.", fr: "Les extensions ajoutent des fonctions au cœur de l’app. Installez un dossier avec plugin.json, activez/désactivez — elles se chargent automatiquement. Elles s’exécutent dans l’app — n’installez que celles de confiance.", es: "Los plugins añaden funciones al núcleo. Instala una carpeta con plugin.json, actívalos/desactívalos y se cargan solos. Se ejecutan en la app — instala solo los de confianza." },
|
"Plugins add features on top of the core app. Browse the catalog below and install what you need — nothing is installed automatically. Toggle plugins on/off; enabled ones load on launch. Plugins run in-app — only install ones you trust.": { ru: "Плагины добавляют функции поверх ядра. Смотри каталог ниже и ставь нужное — ничего не ставится само. Включай/выключай; включённые грузятся при запуске. Плагины работают внутри приложения — ставь только те, которым доверяешь.", de: "Plugins erweitern die Kern-App. Durchsuche den Katalog unten und installiere, was du brauchst — nichts wird automatisch installiert. Plugins ein/aus schalten; aktivierte laden beim Start. Sie laufen in der App — installiere nur vertrauenswürdige.", fr: "Les extensions enrichissent le cœur de l’app. Parcourez le catalogue ci-dessous et installez ce qu’il faut — rien n’est installé automatiquement. Activez/désactivez ; les activées se chargent au démarrage. Elles s’exécutent dans l’app — n’installez que celles de confiance.", es: "Los plugins añaden funciones al núcleo. Explora el catálogo abajo e instala lo que necesites — nada se instala solo. Actívalos/desactívalos; los activos se cargan al iniciar. Se ejecutan en la app — instala solo los de confianza." },
|
||||||
"No plugins installed yet.\nInstall one from a folder, or drop it into the plugins folder below.": { ru: "Плагинов пока нет.\nУстанови из папки или положи в папку плагинов ниже.", de: "Noch keine Plugins.\nAus einem Ordner installieren oder in den Plugin-Ordner unten legen.", fr: "Aucune extension.\nInstallez depuis un dossier ou déposez-la dans le dossier ci-dessous.", es: "Aún no hay plugins.\nInstala desde una carpeta o suéltalo en la carpeta de abajo." },
|
"No plugins installed yet.\nInstall one from a folder, or drop it into the plugins folder below.": { ru: "Плагинов пока нет.\nУстанови из папки или положи в папку плагинов ниже.", de: "Noch keine Plugins.\nAus einem Ordner installieren oder in den Plugin-Ordner unten legen.", fr: "Aucune extension.\nInstallez depuis un dossier ou déposez-la dans le dossier ci-dessous.", es: "Aún no hay plugins.\nInstala desde una carpeta o suéltalo en la carpeta de abajo." },
|
||||||
"Enable": { ru: "Включить", de: "Aktivieren", fr: "Activer", es: "Activar" },
|
"Enable": { ru: "Включить", de: "Aktivieren", fr: "Activer", es: "Activar" },
|
||||||
"Disable": { ru: "Выключить", de: "Deaktivieren", fr: "Désactiver", es: "Desactivar" },
|
"Disable": { ru: "Выключить", de: "Deaktivieren", fr: "Désactiver", es: "Desactivar" },
|
||||||
"Official plugins": { ru: "Официальные плагины", de: "Offizielle Plugins", fr: "Extensions officielles", es: "Plugins oficiales" },
|
"Official plugins": { ru: "Официальные плагины", de: "Offizielle Plugins", fr: "Extensions officielles", es: "Plugins oficiales" },
|
||||||
|
"Available to install": { ru: "Доступно для установки", de: "Verfügbar zur Installation", fr: "Disponibles à l’installation", es: "Disponibles para instalar" },
|
||||||
|
"from the Exbyte plugin registry": { ru: "из реестра плагинов Exbyte", de: "aus der Exbyte-Plugin-Registry", fr: "depuis le registre de plugins Exbyte", es: "del registro de plugins de Exbyte" },
|
||||||
|
"Loading the plugin catalog…": { ru: "Загружаю каталог плагинов…", de: "Plugin-Katalog wird geladen…", fr: "Chargement du catalogue…", es: "Cargando el catálogo…" },
|
||||||
|
"Couldn't reach the plugin registry.": { ru: "Не удалось получить реестр плагинов.", de: "Plugin-Registry nicht erreichbar.", fr: "Registre de plugins injoignable.", es: "No se pudo acceder al registro." },
|
||||||
|
"Retry": { ru: "Повторить", de: "Erneut", fr: "Réessayer", es: "Reintentar" },
|
||||||
|
"Everything from the catalog is already installed.": { ru: "Всё из каталога уже установлено.", de: "Alles aus dem Katalog ist bereits installiert.", fr: "Tout le catalogue est déjà installé.", es: "Todo el catálogo ya está instalado." },
|
||||||
"Install": { ru: "Установить", de: "Installieren", fr: "Installer", es: "Instalar" },
|
"Install": { ru: "Установить", de: "Installieren", fr: "Installer", es: "Instalar" },
|
||||||
"Open plugins folder": { ru: "Открыть папку плагинов", de: "Plugin-Ordner öffnen", fr: "Ouvrir le dossier des extensions", es: "Abrir carpeta de plugins" },
|
"Open plugins folder": { ru: "Открыть папку плагинов", de: "Plugin-Ordner öffnen", fr: "Ouvrir le dossier des extensions", es: "Abrir carpeta de plugins" },
|
||||||
"Reload": { ru: "Перезагрузить", de: "Neu laden", fr: "Recharger", es: "Recargar" },
|
"Reload": { ru: "Перезагрузить", de: "Neu laden", fr: "Recharger", es: "Recargar" },
|
||||||
|
|||||||
@ -113,6 +113,7 @@ export const p4 = {
|
|||||||
pluginReadEntry: (id: string) => invoke<string>("plugin_read_entry", { id }),
|
pluginReadEntry: (id: string) => invoke<string>("plugin_read_entry", { id }),
|
||||||
pluginSetEnabled: (id: string, enabled: boolean) => invoke<void>("plugin_set_enabled", { id, enabled }),
|
pluginSetEnabled: (id: string, enabled: boolean) => invoke<void>("plugin_set_enabled", { id, enabled }),
|
||||||
pluginWriteFile: (id: string, name: string, content: string) => invoke<void>("plugin_write_file", { id, name, content }),
|
pluginWriteFile: (id: string, name: string, content: string) => invoke<void>("plugin_write_file", { id, name, content }),
|
||||||
|
registryHttpGet: (url: string) => invoke<string>("registry_http_get", { url }),
|
||||||
pluginInstall: (src: string) => invoke<string>("plugin_install", { src }),
|
pluginInstall: (src: string) => invoke<string>("plugin_install", { src }),
|
||||||
pluginRemove: (id: string) => invoke<void>("plugin_remove", { id }),
|
pluginRemove: (id: string) => invoke<void>("plugin_remove", { id }),
|
||||||
pluginsDirPath: () => invoke<string>("plugins_dir_path"),
|
pluginsDirPath: () => invoke<string>("plugins_dir_path"),
|
||||||
|
|||||||
@ -11,14 +11,18 @@ import { t } from "./i18n";
|
|||||||
import { resolveTheme, loadActiveId } from "./themes";
|
import { resolveTheme, loadActiveId } from "./themes";
|
||||||
|
|
||||||
// ---- contribution shapes ----------------------------------------------------
|
// ---- contribution shapes ----------------------------------------------------
|
||||||
export interface PluginMenuItem { pluginId: string; label: string; run: () => void | Promise<void> }
|
// `icon` is a NAME from the host icon set (e.g. "ue", "hammer"); the host renders
|
||||||
export interface PluginFileItem { pluginId: string; label: string; run: (file: { depotFile?: string; [k: string]: unknown }) => void | Promise<void> }
|
// the real SVG. Unknown / omitted names fall back to a neutral glyph.
|
||||||
|
export interface PluginMenuItem { pluginId: string; label: string; icon?: string; run: () => void | Promise<void> }
|
||||||
|
export interface PluginFileItem { pluginId: string; label: string; icon?: string; run: (file: { depotFile?: string; [k: string]: unknown }) => void | Promise<void> }
|
||||||
|
export interface PluginFolderItem { pluginId: string; label: string; icon?: string; visible?: () => boolean; run: () => void | Promise<void> }
|
||||||
export interface PluginView { pluginId: string; id: string; title: string; render: (container: HTMLElement) => void | (() => void) }
|
export interface PluginView { pluginId: string; id: string; title: string; render: (container: HTMLElement) => void | (() => void) }
|
||||||
|
export interface PluginDock { pluginId: string; id: string; title: string; icon?: string; render: (container: HTMLElement) => void | (() => void) }
|
||||||
export interface PluginSubmitHook { pluginId: string; run: (files: { depotFile?: string }[]) => SubmitVerdict | Promise<SubmitVerdict> }
|
export interface PluginSubmitHook { pluginId: string; run: (files: { depotFile?: string }[]) => SubmitVerdict | Promise<SubmitVerdict> }
|
||||||
export type SubmitVerdict = { ok: boolean; message?: string };
|
export type SubmitVerdict = { ok: boolean; message?: string };
|
||||||
|
|
||||||
interface Registry { menu: PluginMenuItem[]; fileItems: PluginFileItem[]; views: PluginView[]; submitHooks: PluginSubmitHook[] }
|
interface Registry { menu: PluginMenuItem[]; fileItems: PluginFileItem[]; folderItems: PluginFolderItem[]; views: PluginView[]; docks: PluginDock[]; submitHooks: PluginSubmitHook[] }
|
||||||
const registry: Registry = { menu: [], fileItems: [], views: [], submitHooks: [] };
|
const registry: Registry = { menu: [], fileItems: [], folderItems: [], views: [], docks: [], submitHooks: [] };
|
||||||
let version = 0;
|
let version = 0;
|
||||||
const listeners = new Set<() => void>();
|
const listeners = new Set<() => void>();
|
||||||
function bump() { version++; listeners.forEach((l) => l()); }
|
function bump() { version++; listeners.forEach((l) => l()); }
|
||||||
@ -26,7 +30,7 @@ function bump() { version++; listeners.forEach((l) => l()); }
|
|||||||
export function getRegistry(): Registry { return registry; }
|
export function getRegistry(): Registry { return registry; }
|
||||||
export function registryVersion(): number { return version; }
|
export function registryVersion(): number { return version; }
|
||||||
export function subscribeRegistry(cb: () => void): () => void { listeners.add(cb); return () => { listeners.delete(cb); }; }
|
export function subscribeRegistry(cb: () => void): () => void { listeners.add(cb); return () => { listeners.delete(cb); }; }
|
||||||
function clearRegistry() { registry.menu = []; registry.fileItems = []; registry.views = []; registry.submitHooks = []; }
|
function clearRegistry() { registry.menu = []; registry.fileItems = []; registry.folderItems = []; registry.views = []; registry.docks = []; registry.submitHooks = []; }
|
||||||
|
|
||||||
// Unreal Engine bridge — the core keeps the build machinery + overlay + log
|
// Unreal Engine bridge — the core keeps the build machinery + overlay + log
|
||||||
// reader; the plugin only drives them. Gated by the "ue" permission.
|
// reader; the plugin only drives them. Gated by the "ue" permission.
|
||||||
@ -34,6 +38,7 @@ export interface UeBridge {
|
|||||||
hasProject: () => boolean;
|
hasProject: () => boolean;
|
||||||
uproject: () => string;
|
uproject: () => string;
|
||||||
build: () => void;
|
build: () => void;
|
||||||
|
launch: () => void;
|
||||||
readLog: (tailBytes?: number) => Promise<string>;
|
readLog: (tailBytes?: number) => Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,9 +60,11 @@ export interface HostApi {
|
|||||||
has: (perm: string) => boolean;
|
has: (perm: string) => boolean;
|
||||||
p4: Record<string, (...a: unknown[]) => Promise<unknown>>;
|
p4: Record<string, (...a: unknown[]) => Promise<unknown>>;
|
||||||
ui: {
|
ui: {
|
||||||
addMenuItem: (label: string, run: () => void | Promise<void>) => void;
|
addMenuItem: (label: string, run: () => void | Promise<void>, opts?: { icon?: string }) => void;
|
||||||
addFileContextItem: (label: string, run: (file: { depotFile?: string }) => void | Promise<void>) => void;
|
addFileContextItem: (label: string, run: (file: { depotFile?: string }) => void | Promise<void>, opts?: { icon?: string }) => void;
|
||||||
|
addFolderContextItem: (label: string, run: () => void | Promise<void>, opts?: { icon?: string; visible?: () => boolean }) => void;
|
||||||
addView: (view: { id: string; title: string; render: (el: HTMLElement, host: HostApi) => void | (() => void) }) => void;
|
addView: (view: { id: string; title: string; render: (el: HTMLElement, host: HostApi) => void | (() => void) }) => void;
|
||||||
|
addDockPanel: (dock: { id: string; title: string; icon?: string; render: (el: HTMLElement, host: HostApi) => void | (() => void) }) => void;
|
||||||
addSubmitHook: (run: (files: { depotFile?: string }[]) => SubmitVerdict | Promise<SubmitVerdict>) => void;
|
addSubmitHook: (run: (files: { depotFile?: string }[]) => SubmitVerdict | Promise<SubmitVerdict>) => void;
|
||||||
};
|
};
|
||||||
flash: (text: string, err?: boolean) => void;
|
flash: (text: string, err?: boolean) => void;
|
||||||
@ -95,9 +102,11 @@ function makeHost(info: PluginInfo, ctx: HostContext): HostApi {
|
|||||||
has,
|
has,
|
||||||
p4: p4host,
|
p4: p4host,
|
||||||
ui: {
|
ui: {
|
||||||
addMenuItem: (label, run) => { registry.menu.push({ pluginId: info.id, label, run }); bump(); },
|
addMenuItem: (label, run, opts) => { registry.menu.push({ pluginId: info.id, label, icon: opts?.icon, run }); bump(); },
|
||||||
addFileContextItem: (label, run) => { registry.fileItems.push({ pluginId: info.id, label, run }); bump(); },
|
addFileContextItem: (label, run, opts) => { registry.fileItems.push({ pluginId: info.id, label, icon: opts?.icon, run }); bump(); },
|
||||||
|
addFolderContextItem: (label, run, opts) => { registry.folderItems.push({ pluginId: info.id, label, icon: opts?.icon, visible: opts?.visible, run }); bump(); },
|
||||||
addView: (view) => { registry.views.push({ pluginId: info.id, id: view.id, title: view.title, render: (el) => view.render(el, host) }); bump(); },
|
addView: (view) => { registry.views.push({ pluginId: info.id, id: view.id, title: view.title, render: (el) => view.render(el, host) }); bump(); },
|
||||||
|
addDockPanel: (dock) => { registry.docks.push({ pluginId: info.id, id: dock.id, title: dock.title, icon: dock.icon, render: (el) => dock.render(el, host) }); bump(); },
|
||||||
addSubmitHook: (run) => { registry.submitHooks.push({ pluginId: info.id, run }); bump(); },
|
addSubmitHook: (run) => { registry.submitHooks.push({ pluginId: info.id, run }); bump(); },
|
||||||
},
|
},
|
||||||
flash: ctx.flash,
|
flash: ctx.flash,
|
||||||
@ -170,19 +179,19 @@ export interface RegistryEntry {
|
|||||||
files: string[]; baseUrl: string;
|
files: string[]; baseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All registry HTTP goes through the Rust backend (registryHttpGet): the webview
|
||||||
|
// `fetch()` is blocked by CORS because Gitea's raw endpoint sends no ACAO header.
|
||||||
export async function fetchRegistry(): Promise<RegistryEntry[]> {
|
export async function fetchRegistry(): Promise<RegistryEntry[]> {
|
||||||
const r = await fetch(REGISTRY_URL, { cache: "no-store" });
|
const txt = await p4.registryHttpGet(REGISTRY_URL);
|
||||||
if (!r.ok) throw new Error(`registry HTTP ${r.status}`);
|
const j = JSON.parse(txt);
|
||||||
const j = await r.json();
|
|
||||||
return Array.isArray(j.plugins) ? (j.plugins as RegistryEntry[]).filter((p) => p && p.id && Array.isArray(p.files)) : [];
|
return Array.isArray(j.plugins) ? (j.plugins as RegistryEntry[]).filter((p) => p && p.id && Array.isArray(p.files)) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Download a registry plugin's files over HTTPS and write them into the plugins folder. */
|
/** Download a registry plugin's files (backend HTTPS) and write them into the plugins folder. */
|
||||||
export async function installFromRegistry(entry: RegistryEntry): Promise<void> {
|
export async function installFromRegistry(entry: RegistryEntry): Promise<void> {
|
||||||
for (const f of entry.files) {
|
for (const f of entry.files) {
|
||||||
const res = await fetch(entry.baseUrl + f, { cache: "no-store" });
|
const text = await p4.registryHttpGet(entry.baseUrl + f);
|
||||||
if (!res.ok) throw new Error(`${f}: HTTP ${res.status}`);
|
await p4.pluginWriteFile(entry.id, f, text);
|
||||||
await p4.pluginWriteFile(entry.id, f, await res.text());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user