0.3.9 — fix plugin registry (backend HTTPS, CORS), fold updater into a status-bar icon
- Plugin catalog/install failed silently: the webview fetch() to Gitea raw is blocked by CORS (no ACAO header). Route registry reads through a new Rust command registry_http_get (host-restricted) — catalog + installs now work. - Updater: removed the floating card that overlapped the Terminal/Log/Updates buttons. Update state now shows as a single status-bar download icon (no text): idle → download glyph, available → accent + dot, checking/downloading → spinner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "exbyte-depot",
|
||||
"private": true,
|
||||
"version": "0.3.8",
|
||||
"version": "0.3.9",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
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"
|
||||
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]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
@ -558,6 +575,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
@ -966,9 +992,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "exbyte-depot"
|
||||
version = "0.3.7"
|
||||
version = "0.3.8"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@ -1279,8 +1306,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1302,8 +1331,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"rand_core 0.10.1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1572,6 +1604,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2042,6 +2075,12 @@ version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "lru-slab"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||
|
||||
[[package]]
|
||||
name = "mac-notification-sys"
|
||||
version = "0.6.15"
|
||||
@ -2784,6 +2823,62 @@ dependencies = [
|
||||
"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]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
@ -2812,7 +2907,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||
dependencies = [
|
||||
"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]]
|
||||
@ -2822,7 +2928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2834,6 +2940,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "raw-window-handle"
|
||||
version = "0.6.2"
|
||||
@ -2909,6 +3030,44 @@ version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "reqwest"
|
||||
version = "0.13.4"
|
||||
@ -3046,6 +3205,7 @@ version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
|
||||
dependencies = [
|
||||
"web-time",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
@ -3093,6 +3253,12 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@ -3315,6 +3481,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "serde_with"
|
||||
version = "3.21.0"
|
||||
@ -3385,7 +3563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"cpufeatures 0.2.17",
|
||||
"digest",
|
||||
]
|
||||
|
||||
@ -3709,7 +3887,7 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
"plist",
|
||||
"raw-window-handle",
|
||||
"reqwest",
|
||||
"reqwest 0.13.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@ -3858,7 +4036,7 @@ checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify-rust",
|
||||
"rand",
|
||||
"rand 0.9.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
@ -3917,7 +4095,7 @@ dependencies = [
|
||||
"minisign-verify",
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"reqwest 0.13.4",
|
||||
"rustls",
|
||||
"semver",
|
||||
"serde",
|
||||
@ -4692,6 +4870,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "web_atoms"
|
||||
version = "0.2.5"
|
||||
@ -4757,6 +4945,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "webview2-com"
|
||||
version = "0.38.2"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "exbyte-depot"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
description = "Exbyte Depot — native Perforce client by Exbyte Studios"
|
||||
authors = ["Exbyte Studios"]
|
||||
edition = "2021"
|
||||
@ -26,6 +26,8 @@ serde_json = "1"
|
||||
tauri-plugin-dialog = "2.7.1"
|
||||
# decode p4 output on non-UTF8 (ANSI/cp1251) systems — Cyrillic paths
|
||||
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)
|
||||
[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(())
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
std::fs::create_dir_all(to)?;
|
||||
for e in std::fs::read_dir(from)? {
|
||||
@ -3146,6 +3165,7 @@ pub fn run() {
|
||||
plugin_read_entry,
|
||||
plugin_set_enabled,
|
||||
plugin_write_file,
|
||||
registry_http_get,
|
||||
plugin_install,
|
||||
plugin_remove,
|
||||
plugins_dir_path,
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Exbyte Depot",
|
||||
"mainBinaryName": "Exbyte Depot",
|
||||
"version": "0.3.8",
|
||||
"version": "0.3.9",
|
||||
"identifier": "com.bonchellon.exbyte-depot",
|
||||
"build": {
|
||||
"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}
|
||||
.stlog:hover{color:var(--txt);border-color:var(--accent)}
|
||||
.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;
|
||||
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);
|
||||
|
||||
22
src/App.tsx
22
src/App.tsx
@ -7,7 +7,7 @@ import { isPermissionGranted, requestPermission, sendNotification } from "@tauri
|
||||
import { open as openDialog } from "@tauri-apps/plugin-dialog";
|
||||
import ModelViewer from "./ModelViewer";
|
||||
import CodeView from "./CodeView";
|
||||
import UpdateBanner from "./Updater";
|
||||
import { useUpdater } from "./Updater";
|
||||
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 { t, LANG, LANGS, setLangGlobal, Lang } from "./i18n";
|
||||
@ -151,7 +151,7 @@ export default function App() {
|
||||
onInfo={setInfo} onSession={(s) => { setSession(s); saveSession(s); }}
|
||||
onDisconnect={() => { p4.disconnect(); clearSession(); setInfo(null); setSession(null); setPhase("connect"); }} />;
|
||||
|
||||
return <>{content}<UpdateBanner /></>;
|
||||
return content;
|
||||
}
|
||||
|
||||
/* ---------------- custom styled dropdown (replaces the ugly native <select>) ---------------- */
|
||||
@ -423,6 +423,7 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
||||
// bottom dock: a tabbed panel (Log / Terminal + any plugin dock panels), opened from Window menu
|
||||
const [dockOpen, setDockOpen] = useState(false);
|
||||
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.
|
||||
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 {}
|
||||
@ -1711,9 +1712,20 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
||||
<span className="stspace" />
|
||||
<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={() => window.dispatchEvent(new Event("exd-check-update"))} title={t("Check for updates")}>
|
||||
<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>{t("Updates")}</span>
|
||||
<button
|
||||
className={"stlog stupd icon" + (upd.phase === "available" ? " has" : "") + (upd.phase === "downloading" || upd.phase === "checking" ? " busy" : "")}
|
||||
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>
|
||||
{peek && logs.length > 0 && (
|
||||
<div className="logpeek" onMouseEnter={showPeek} onMouseLeave={hidePeek}>
|
||||
|
||||
118
src/Updater.tsx
118
src/Updater.tsx
@ -1,53 +1,44 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { check, type Update } from "@tauri-apps/plugin-updater";
|
||||
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
|
||||
// launch, and again on demand when the "Updates" button fires `exd-check-update`.
|
||||
// If a newer signed version exists, slides a small card into the bottom-right.
|
||||
export default function UpdateBanner() {
|
||||
export interface UpdaterState {
|
||||
phase: UpdPhase;
|
||||
pct: number;
|
||||
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 [phase, setPhase] = useState<Phase>("idle");
|
||||
const [phase, setPhase] = useState<UpdPhase>("idle");
|
||||
const [pct, setPct] = useState(0);
|
||||
const [err, setErr] = useState("");
|
||||
const [hidden, setHidden] = useState(false);
|
||||
const busyRef = useRef(false);
|
||||
|
||||
// shared check routine. `manual` shows "checking" / "up to date" feedback.
|
||||
async function runCheck(manual: boolean) {
|
||||
if (busyRef.current || phase === "downloading") return;
|
||||
busyRef.current = true;
|
||||
if (manual) { setHidden(false); setPhase("checking"); }
|
||||
if (manual) setPhase("checking");
|
||||
try {
|
||||
const u = await check();
|
||||
if (u) { setUpdate(u); setPhase("available"); setHidden(false); }
|
||||
else if (manual) { setPhase("uptodate"); setHidden(false); }
|
||||
else setPhase("idle");
|
||||
if (u) { setUpdate(u); setPhase("available"); }
|
||||
else { setPhase(manual ? "uptodate" : "idle"); }
|
||||
} catch (e) {
|
||||
if (manual) { setErr(String(e)); setPhase("error"); setHidden(false); }
|
||||
setErr(String(e));
|
||||
if (manual) setPhase("error");
|
||||
} 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() {
|
||||
if (!update) return;
|
||||
if (!update) { void runCheck(true); return; }
|
||||
setPhase("downloading"); setPct(0);
|
||||
let total = 0, got = 0;
|
||||
try {
|
||||
@ -61,59 +52,20 @@ export default function UpdateBanner() {
|
||||
} 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)
|
||||
if (phase === "checking") {
|
||||
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") {
|
||||
return <div className="upd sm ok" onClick={() => { setHidden(true); setPhase("idle"); }}>
|
||||
<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>
|
||||
</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;
|
||||
// let the transient "checked, up to date" / "error" state fade back to idle
|
||||
useEffect(() => {
|
||||
if (phase !== "uptodate" && phase !== "error") return;
|
||||
const id = setTimeout(() => setPhase("idle"), 3500);
|
||||
return () => clearTimeout(id);
|
||||
}, [phase]);
|
||||
|
||||
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>
|
||||
);
|
||||
return { phase, pct, err, version: update?.version, check: () => runCheck(true), install };
|
||||
}
|
||||
|
||||
@ -180,6 +180,7 @@ const D: Record<string, Tr> = {
|
||||
"Plugins": { ru: "Плагины", de: "Plugins", fr: "Extensions", es: "Plugins" },
|
||||
"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" },
|
||||
"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" },
|
||||
"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" },
|
||||
|
||||
@ -113,6 +113,7 @@ export const p4 = {
|
||||
pluginReadEntry: (id: string) => invoke<string>("plugin_read_entry", { id }),
|
||||
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 }),
|
||||
registryHttpGet: (url: string) => invoke<string>("registry_http_get", { url }),
|
||||
pluginInstall: (src: string) => invoke<string>("plugin_install", { src }),
|
||||
pluginRemove: (id: string) => invoke<void>("plugin_remove", { id }),
|
||||
pluginsDirPath: () => invoke<string>("plugins_dir_path"),
|
||||
|
||||
@ -179,19 +179,19 @@ export interface RegistryEntry {
|
||||
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[]> {
|
||||
const r = await fetch(REGISTRY_URL, { cache: "no-store" });
|
||||
if (!r.ok) throw new Error(`registry HTTP ${r.status}`);
|
||||
const j = await r.json();
|
||||
const txt = await p4.registryHttpGet(REGISTRY_URL);
|
||||
const j = JSON.parse(txt);
|
||||
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> {
|
||||
for (const f of entry.files) {
|
||||
const res = await fetch(entry.baseUrl + f, { cache: "no-store" });
|
||||
if (!res.ok) throw new Error(`${f}: HTTP ${res.status}`);
|
||||
await p4.pluginWriteFile(entry.id, f, await res.text());
|
||||
const text = await p4.registryHttpGet(entry.baseUrl + f);
|
||||
await p4.pluginWriteFile(entry.id, f, text);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user