0.3.6 — plugin registry, Unreal extracted to a plugin, in-app update check + fixes
- Official plugin registry hosted on Gitea; client fetches registry.json over HTTPS and installs plugins via backend plugin_write_file. - Auto-install autoInstall plugins on first connect; per-machine enabled state. - Extract all Unreal tooling (Build Solution / Unreal Log) into a separate "unreal" plugin (defaultEnabled) driven through the new host.ue bridge; core UI no longer hard-wires Unreal. - Settings → Plugins: "Official plugins" section with Install buttons. - Status bar: "Updates" button (right of Log) triggers an update check without restarting the client. - Fixes: NEW badge hard-right/centered without shifting text; History commit size no longer flickers (cached per change); giant icons in empty plugin/ People lists; Get Latest hover contrast. 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.5",
|
||||
"version": "0.3.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
46
plugins-examples/unreal/index.js
Normal file
46
plugins-examples/unreal/index.js
Normal file
@ -0,0 +1,46 @@
|
||||
// Unreal Engine Tools — official plugin.
|
||||
// The core app keeps the build engine (UnrealBuildTool / MSBuild), the build
|
||||
// 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
|
||||
// permission-gated `host.ue` bridge.
|
||||
|
||||
export function activate(host) {
|
||||
const ue = host.ue;
|
||||
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++).
|
||||
host.ui.addMenuItem("🔨 Build Solution (Unreal)", () => {
|
||||
if (!ue.hasProject()) { host.flash("No Unreal project in the current working folder.", true); return; }
|
||||
ue.build(); // triggers the core build flow + overlay
|
||||
});
|
||||
|
||||
// Live Unreal editor log (tails Saved/Logs). Opens from the Actions menu.
|
||||
host.ui.addView({
|
||||
id: "unreal-log",
|
||||
title: "Unreal Log",
|
||||
render: (el) => {
|
||||
el.style.cssText = "display:flex;flex-direction:column;height:100%;min-height:340px";
|
||||
const pre = document.createElement("pre");
|
||||
pre.style.cssText =
|
||||
"flex:1;margin:0;overflow:auto;white-space:pre-wrap;word-break:break-word;" +
|
||||
"font:11.5px/1.6 var(--mono,ui-monospace,monospace);color:var(--muted,#9a9aa8)";
|
||||
el.appendChild(pre);
|
||||
let alive = true;
|
||||
async function tick() {
|
||||
if (!alive) return;
|
||||
try {
|
||||
if (!ue.hasProject()) { pre.textContent = "No Unreal project in the current working folder."; return; }
|
||||
const atBottom = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 30;
|
||||
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).";
|
||||
if (atBottom) pre.scrollTop = pre.scrollHeight;
|
||||
} catch (e) { pre.textContent = String(e); }
|
||||
}
|
||||
tick();
|
||||
const id = setInterval(tick, 1500);
|
||||
return () => { alive = false; clearInterval(id); };
|
||||
},
|
||||
});
|
||||
|
||||
host.log("Unreal Engine Tools activated");
|
||||
}
|
||||
12
plugins-examples/unreal/plugin.json
Normal file
12
plugins-examples/unreal/plugin.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "unreal",
|
||||
"name": "Unreal Engine Tools",
|
||||
"version": "1.0.0",
|
||||
"author": "Exbyte Studios",
|
||||
"description": "Build the game (.sln / UnrealBuildTool) and tail the Unreal editor log. Enable this on Unreal Engine projects.",
|
||||
"entry": "index.js",
|
||||
"minAppVersion": "0.3.6",
|
||||
"permissions": ["ue"],
|
||||
"defaultEnabled": true,
|
||||
"contributes": { "menu": true, "views": ["unreal-log"] }
|
||||
}
|
||||
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -966,7 +966,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "exbyte-depot"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"serde",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "exbyte-depot"
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
description = "Exbyte Depot — native Perforce client by Exbyte Studios"
|
||||
authors = ["Exbyte Studios"]
|
||||
edition = "2021"
|
||||
|
||||
@ -2923,7 +2923,10 @@ async fn plugin_list() -> Result<Vec<Value>, String> {
|
||||
let Ok(m) = serde_json::from_str::<Value>(&txt) else { continue };
|
||||
let id = m.get("id").and_then(|v| v.as_str()).map(String::from)
|
||||
.unwrap_or_else(|| e.file_name().to_string_lossy().to_string());
|
||||
let enabled = *en.get(&id).unwrap_or(&true);
|
||||
// off by default unless the manifest opts in (defaultEnabled) — official
|
||||
// plugins like Unreal ship on; the user's choice persists in enabled.json
|
||||
let default_on = m.get("defaultEnabled").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
let enabled = *en.get(&id).unwrap_or(&default_on);
|
||||
out.push(serde_json::json!({
|
||||
"id": id,
|
||||
"name": m.get("name").and_then(|v| v.as_str()).unwrap_or(""),
|
||||
@ -2967,6 +2970,23 @@ async fn plugin_set_enabled(id: String, enabled: bool) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write one file of a plugin into <config>/plugins/<id>/<name>. The frontend
|
||||
/// fetches the file bytes from the registry over HTTPS and hands the content here;
|
||||
/// this only touches the confined plugins folder (id + name are path-validated).
|
||||
#[tauri::command]
|
||||
async fn plugin_write_file(id: String, name: String, content: String) -> Result<(), String> {
|
||||
if id.is_empty() || !id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.') || id.contains("..") {
|
||||
return Err("Invalid plugin id".into());
|
||||
}
|
||||
if name.is_empty() || name.contains('/') || name.contains('\\') || name.contains("..") {
|
||||
return Err("Invalid file name".into());
|
||||
}
|
||||
let dir = plugins_dir().ok_or("No config dir")?.join(&id);
|
||||
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
std::fs::write(dir.join(&name), content).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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)? {
|
||||
@ -3125,6 +3145,7 @@ pub fn run() {
|
||||
plugin_list,
|
||||
plugin_read_entry,
|
||||
plugin_set_enabled,
|
||||
plugin_write_file,
|
||||
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.5",
|
||||
"version": "0.3.6",
|
||||
"identifier": "com.bonchellon.exbyte-depot",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@ -24,8 +24,8 @@
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src 'self'; script-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; media-src 'self' blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost https://openrouter.ai; worker-src 'self' blob:; object-src 'none'; base-uri 'self'",
|
||||
"devCsp": "default-src 'self'; script-src 'self' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; media-src 'self' blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost ws://localhost:1420 http://localhost:1420 https://openrouter.ai; worker-src 'self' blob:; object-src 'none'; base-uri 'self'"
|
||||
"csp": "default-src 'self'; script-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; media-src 'self' blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost https://openrouter.ai https://gitea.exbytestudios.com; worker-src 'self' blob:; object-src 'none'; base-uri 'self'",
|
||||
"devCsp": "default-src 'self'; script-src 'self' 'unsafe-inline' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: http://asset.localhost; media-src 'self' blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost ws://localhost:1420 http://localhost:1420 https://openrouter.ai https://gitea.exbytestudios.com; worker-src 'self' blob:; object-src 'none'; base-uri 'self'"
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
23
src/App.css
23
src/App.css
@ -94,7 +94,11 @@ input,textarea,[contenteditable="true"],
|
||||
.tgroup{display:flex;min-width:0;position:relative}
|
||||
.tgroup .tzone{flex:1;min-width:0}
|
||||
.tgroup .tzone:last-child{flex:0 0 var(--right-zone-w,220px);min-width:160px;border-right:none}
|
||||
.tzone:last-child{border-right:none}.tzone:hover{background:rgba(124,110,246,.06)}
|
||||
.tzone:last-child{border-right:none}
|
||||
.tzone:hover{background:var(--hover)}
|
||||
/* keep the labels/icon high-contrast on hover in every theme (fixes greyed-out text) */
|
||||
.tzone:hover .val{color:var(--txt)}.tzone:hover .lbl,.tzone:hover .sub{color:var(--muted)}
|
||||
.sync:hover .ic{color:var(--accent-2)}.sync:hover .val{color:var(--txt)}.sync:hover .sub{color:var(--muted)}
|
||||
|
||||
/* draggable panel dividers */
|
||||
.vhandle{position:absolute;top:0;bottom:0;width:9px;cursor:col-resize;z-index:8}
|
||||
@ -120,9 +124,11 @@ body.resizing{cursor:col-resize!important;user-select:none}
|
||||
.tab{flex:1;display:flex;align-items:center;justify-content:center;gap:8px;font-size:13px;font-weight:600;
|
||||
color:var(--muted);cursor:pointer;position:relative;transition:color .2s;background:none;border:none}
|
||||
.tab:hover{color:var(--txt)}.tab.on{color:var(--txt)}
|
||||
.tab:focus{outline:none}
|
||||
.tab:focus-visible{outline:none;box-shadow:inset 0 0 0 2px rgba(124,110,246,.35)}
|
||||
.tab:active{transform:scale(.97)}
|
||||
/* sliding underline that glides between the two tabs */
|
||||
.tab-slider{position:absolute;bottom:-1px;height:2px;width:calc(50% - 32px);border-radius:2px 2px 0 0;pointer-events:none;
|
||||
.tab-slider{position:absolute;bottom:0;height:2px;width:calc(50% - 32px);border-radius:2px;pointer-events:none;
|
||||
background:linear-gradient(90deg,var(--accent),var(--accent-2));box-shadow:0 0 10px rgba(124,110,246,.6);
|
||||
transition:left .34s cubic-bezier(.5,.02,.15,1)}
|
||||
/* content fades + rises slightly when the tab changes */
|
||||
@ -276,6 +282,10 @@ body.resizing{cursor:col-resize!important;user-select:none}
|
||||
.upd-ic{width:34px;height:34px;flex:0 0 auto;border-radius:10px;display:grid;place-items:center;color:#fff;
|
||||
background:linear-gradient(145deg,var(--accent-2),var(--accent-deep));box-shadow:0 0 18px rgba(124,110,246,.5)}
|
||||
.upd-ic svg{width:18px;height:18px}
|
||||
.upd.sm{width:auto;max-width:340px;padding:11px 14px}
|
||||
.upd.sm .upd-ic{width:28px;height:28px}
|
||||
.upd.sm.ok{cursor:pointer}
|
||||
.upd-ic.ok{background:linear-gradient(145deg,var(--add),#1f9f6e);box-shadow:0 0 16px rgba(62,207,142,.45)}
|
||||
.upd-ttl{display:flex;flex-direction:column;gap:1px;min-width:0}
|
||||
.upd-ttl b{font-size:13.5px}
|
||||
.upd-ttl span{font-size:11.5px;color:var(--faint);font-variant-numeric:tabular-nums}
|
||||
@ -721,6 +731,7 @@ body.resizing-v{cursor:row-resize!important;user-select:none}
|
||||
.th-token input[type=color]::-webkit-color-swatch-wrapper{padding:0}
|
||||
/* plugins manager */
|
||||
.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-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-main{flex:1;min-width:0}
|
||||
@ -785,7 +796,7 @@ body.resizing-v{cursor:row-resize!important;user-select:none}
|
||||
.diff::-webkit-scrollbar{width:10px}.diff::-webkit-scrollbar-thumb{background:var(--panel-3);border-radius:8px}
|
||||
|
||||
/* history */
|
||||
.hrow{display:flex;gap:12px;align-items:flex-start;padding:12px 16px;border-bottom:1px solid var(--border-soft)}
|
||||
.hrow{display:flex;gap:12px;align-items:flex-start;padding:12px 16px;border-bottom:1px solid var(--border-soft);position:relative}
|
||||
.hrow.click{cursor:pointer}
|
||||
.hrow:hover{background:var(--hover)}
|
||||
.hrow.sel{background:rgba(124,110,246,.12);box-shadow:inset 2px 0 0 var(--accent)}
|
||||
@ -846,7 +857,7 @@ body.resizing-v{cursor:row-resize!important;user-select:none}
|
||||
.hdesc{font-size:13px;color:var(--txt);overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
||||
.hmeta{font-size:11px;color:var(--faint)}
|
||||
.hsize{margin-left:7px;padding:1px 6px;border-radius:6px;background:var(--chip);border:1px solid var(--border-soft);color:var(--muted);font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap}
|
||||
.hnew{display:inline-block;vertical-align:middle;margin-right:7px;padding:1px 7px;border-radius:6px;font-size:9.5px;font-weight:800;letter-spacing:.6px;color:#fff;
|
||||
.hnew{position:absolute;top:50%;right:14px;transform:translateY(-50%);padding:3px 10px;border-radius:7px;font-size:11px;font-weight:800;letter-spacing:.7px;color:#fff;pointer-events:none;
|
||||
background:linear-gradient(135deg,var(--accent-2),var(--accent-deep));box-shadow:0 0 0 rgba(124,110,246,.5);animation:newpulse 2.2s ease-in-out infinite}
|
||||
@keyframes newpulse{0%,100%{box-shadow:0 0 0 0 rgba(124,110,246,.45)}50%{box-shadow:0 0 0 4px rgba(124,110,246,0)}}
|
||||
|
||||
@ -1110,7 +1121,9 @@ body.resizing-v{cursor:row-resize!important;user-select:none}
|
||||
.ur-actd{width:8px;height:8px;border-radius:50%;flex:0 0 auto;background:var(--faint)}
|
||||
.ur-actd.active{background:#3ecb8f;box-shadow:0 0 0 3px rgba(62,203,143,.18)}
|
||||
.ur-actd.recent{background:#f4c04e}.ur-actd.idle{background:#4b4b58}
|
||||
.ur-empty{padding:16px;font-size:12px;color:var(--faint);text-align:center}
|
||||
.ur-empty{padding:16px;font-size:12px;color:var(--faint);text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;white-space:pre-line}
|
||||
.ur-empty svg{width:34px;height:34px;opacity:.35;flex:0 0 auto}
|
||||
.viewer-msg svg{width:36px;height:36px;opacity:.4;flex:0 0 auto}
|
||||
.ur-detail{flex:1;display:flex;flex-direction:column;min-width:0;overflow-y:auto}
|
||||
.ur-dhead{display:flex;align-items:center;gap:14px;padding:18px 20px 14px;border-bottom:1px solid var(--border-soft)}
|
||||
.ur-dinfo{min-width:0}
|
||||
|
||||
106
src/App.tsx
106
src/App.tsx
@ -12,7 +12,7 @@ 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";
|
||||
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 } from "./plugins";
|
||||
import { loadPlugins, getRegistry, subscribeRegistry, runSubmitHooks, PluginView, autoInstallOfficial, fetchRegistry, installFromRegistry, RegistryEntry } from "./plugins";
|
||||
import { aiSummary, aiExplainCode, getExplain, saveExplain, dropExplain, initials, avatarColor, activity } from "./ai";
|
||||
|
||||
// native OS notification (works even when the window is hidden in the tray)
|
||||
@ -378,6 +378,8 @@ function Workbench({ info, session, light, toggleTheme, themeId, setThemeId, lan
|
||||
const [toast, setToast] = useState<{ text: string; err?: boolean } | null>(null);
|
||||
const [history, setHistory] = useState<Change[]>([]);
|
||||
const [changeSizes, setChangeSizes] = useState<Record<string, number>>({}); // per-changelist byte weight for History
|
||||
const sizesRef = useRef<Record<string, number>>({}); // mirror of changeSizes so we only fetch new changes (no flicker)
|
||||
const histSizeScope = useRef<string>(" | ||||