515 lines
18 KiB
JavaScript
515 lines
18 KiB
JavaScript
(() => {
|
|
const IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico", ".bmp"]);
|
|
const MD_EXTS = /* @__PURE__ */ new Set([".md", ".markdown", ".mdown"]);
|
|
const DOWNLOAD_EXTS = /* @__PURE__ */ new Set([
|
|
".docx",
|
|
".doc",
|
|
".xlsx",
|
|
".xls",
|
|
".pptx",
|
|
".ppt",
|
|
".odt",
|
|
".ods",
|
|
".odp",
|
|
".pdf",
|
|
".zip",
|
|
".tar",
|
|
".gz",
|
|
".bz2",
|
|
".7z",
|
|
".rar",
|
|
".mp3",
|
|
".mp4",
|
|
".wav",
|
|
".m4a",
|
|
".ogg",
|
|
".flac",
|
|
".mov",
|
|
".avi",
|
|
".mkv",
|
|
".webm",
|
|
".exe",
|
|
".dmg",
|
|
".pkg",
|
|
".deb",
|
|
".rpm",
|
|
".woff",
|
|
".woff2",
|
|
".ttf",
|
|
".otf",
|
|
".eot",
|
|
".bin",
|
|
".dat",
|
|
".db",
|
|
".sqlite",
|
|
".pyc",
|
|
".class",
|
|
".so",
|
|
".dylib",
|
|
".dll"
|
|
]);
|
|
window.IMAGE_EXTS = IMAGE_EXTS;
|
|
window.MD_EXTS = MD_EXTS;
|
|
window.DOWNLOAD_EXTS = DOWNLOAD_EXTS;
|
|
function fileExt(p) {
|
|
const i = p.lastIndexOf(".");
|
|
return i >= 0 ? p.slice(i).toLowerCase() : "";
|
|
}
|
|
window.fileExt = fileExt;
|
|
async function api(path, opts = {}) {
|
|
const rel = path.startsWith("/") ? path.slice(1) : path;
|
|
const url = new URL(rel, location.href);
|
|
const res = await fetch(url.href, { credentials: "include", headers: { "Content-Type": "application/json" }, ...opts });
|
|
if (!res.ok) {
|
|
const text = await res.text();
|
|
try {
|
|
const j = JSON.parse(text);
|
|
throw new Error(j.error || j.message || text);
|
|
} catch (e) {
|
|
if (e instanceof SyntaxError) throw new Error(text);
|
|
throw e;
|
|
}
|
|
}
|
|
const ct = res.headers.get("content-type") || "";
|
|
return ct.includes("application/json") ? res.json() : res.text();
|
|
}
|
|
window.api = api;
|
|
function _wsExpandKey() {
|
|
const ws = S.session && S.session.workspace;
|
|
return ws ? "hermes-webui-expanded:" + ws : null;
|
|
}
|
|
function _saveExpandedDirs() {
|
|
const key = _wsExpandKey();
|
|
if (!key) return;
|
|
try {
|
|
localStorage.setItem(key, JSON.stringify([...S._expandedDirs || /* @__PURE__ */ new Set()]));
|
|
} catch (e) {
|
|
}
|
|
}
|
|
function _restoreExpandedDirs() {
|
|
const key = _wsExpandKey();
|
|
if (!key) {
|
|
S._expandedDirs = /* @__PURE__ */ new Set();
|
|
return;
|
|
}
|
|
try {
|
|
const raw = localStorage.getItem(key);
|
|
S._expandedDirs = raw ? new Set(JSON.parse(raw)) : /* @__PURE__ */ new Set();
|
|
} catch (e) {
|
|
S._expandedDirs = /* @__PURE__ */ new Set();
|
|
}
|
|
}
|
|
async function loadDir(path) {
|
|
if (!S.session) return;
|
|
try {
|
|
if (!path || path === ".") {
|
|
S._dirCache = {};
|
|
_restoreExpandedDirs();
|
|
}
|
|
S.currentDir = path || ".";
|
|
const data = await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
|
|
S.entries = data.entries || [];
|
|
renderBreadcrumb();
|
|
renderFileTree();
|
|
if (!path || path === ".") {
|
|
for (const dirPath of S._expandedDirs || []) {
|
|
if (!S._dirCache[dirPath]) {
|
|
try {
|
|
const dc = await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(dirPath)}`);
|
|
S._dirCache[dirPath] = dc.entries || [];
|
|
} catch (e2) {
|
|
S._dirCache[dirPath] = [];
|
|
}
|
|
}
|
|
}
|
|
if (S._expandedDirs && S._expandedDirs.size > 0) renderFileTree();
|
|
}
|
|
if (typeof clearPreview === "function") {
|
|
if (typeof _previewDirty !== "undefined" && _previewDirty) {
|
|
showConfirmDialog({ title: t("unsaved_confirm"), message: "", confirmLabel: "Discard", danger: true, focusCancel: true }).then((ok) => {
|
|
if (ok) clearPreview();
|
|
});
|
|
} else {
|
|
clearPreview();
|
|
}
|
|
}
|
|
if (!path || path === ".") _refreshGitBadge();
|
|
} catch (e) {
|
|
console.warn("loadDir", e);
|
|
}
|
|
}
|
|
async function _refreshGitBadge() {
|
|
const badge = $("gitBadge");
|
|
if (!badge || !S.session) return;
|
|
try {
|
|
const data = await api(`/api/git-info?session_id=${encodeURIComponent(S.session.session_id)}`);
|
|
if (data.git && data.git.is_git) {
|
|
const g = data.git;
|
|
let text = g.branch || "git";
|
|
if ((g.dirty ?? 0) > 0) text += ` \xB7 ${g.dirty}\u2206`;
|
|
if ((g.behind ?? 0) > 0) text += ` \u2193${g.behind}`;
|
|
if ((g.ahead ?? 0) > 0) text += ` \u2191${g.ahead}`;
|
|
badge.textContent = text;
|
|
badge.className = "git-badge" + ((g.dirty ?? 0) > 0 ? " dirty" : "");
|
|
badge.style.display = "";
|
|
} else {
|
|
badge.style.display = "none";
|
|
badge.textContent = "";
|
|
}
|
|
} catch (e) {
|
|
if (badge) badge.style.display = "none";
|
|
}
|
|
}
|
|
function navigateUp() {
|
|
if (!S.session || S.currentDir === ".") return;
|
|
const parts = S.currentDir.split("/");
|
|
parts.pop();
|
|
loadDir(parts.length ? parts.join("/") : ".");
|
|
}
|
|
let _previewCurrentPath = "";
|
|
let _previewCurrentMode = "";
|
|
let _previewDirty = false;
|
|
let _previewRawContent = "";
|
|
function showPreview(mode) {
|
|
const codeEl = $("previewCode");
|
|
const imgWrap = $("previewImgWrap");
|
|
const mdEl = $("previewMd");
|
|
const editEl = $("previewEditArea");
|
|
const badge = $("previewBadge");
|
|
if (codeEl) codeEl.style.display = mode === "code" ? "" : "none";
|
|
if (imgWrap) imgWrap.style.display = mode === "image" ? "" : "none";
|
|
if (mdEl) mdEl.style.display = mode === "md" ? "" : "none";
|
|
if (editEl) editEl.style.display = "none";
|
|
if (badge) {
|
|
badge.className = "preview-badge " + mode;
|
|
const pathText = $("previewPathText");
|
|
badge.textContent = mode === "image" ? "image" : mode === "md" ? "md" : fileExt(pathText?.textContent || "");
|
|
}
|
|
_previewCurrentMode = mode;
|
|
_previewDirty = false;
|
|
updateEditBtn();
|
|
}
|
|
function updateEditBtn() {
|
|
const btn = $("btnEditFile");
|
|
if (!btn) return;
|
|
const editable = _previewCurrentMode === "code" || _previewCurrentMode === "md";
|
|
btn.style.display = editable ? "" : "none";
|
|
const editing = $("previewEditArea")?.style.display !== "none";
|
|
btn.innerHTML = editing ? `💾 ${t("save")}` : `✎ ${t("edit")}`;
|
|
btn.title = editing ? t("save_title") : t("edit_title");
|
|
btn.style.color = editing ? "var(--blue)" : "";
|
|
if (_previewDirty) btn.innerHTML = "💾 Save*";
|
|
}
|
|
async function toggleEditMode() {
|
|
const editEl = $("previewEditArea");
|
|
const editing = editEl && editEl.style.display !== "none";
|
|
if (editing) {
|
|
if (!S.session || !_previewCurrentPath) return;
|
|
const content = editEl.value;
|
|
try {
|
|
await api("/api/file/save", { method: "POST", body: JSON.stringify({ session_id: S.session.session_id, path: _previewCurrentPath, content }) });
|
|
_previewDirty = false;
|
|
const codeEl = $("previewCode");
|
|
const mdEl = $("previewMd");
|
|
if (_previewCurrentMode === "code" && codeEl) codeEl.textContent = content;
|
|
else if (mdEl) {
|
|
mdEl.innerHTML = renderMd(content);
|
|
requestAnimationFrame(() => {
|
|
if (typeof renderKatexBlocks === "function") renderKatexBlocks();
|
|
});
|
|
}
|
|
if (editEl) editEl.style.display = "none";
|
|
if (codeEl && _previewCurrentMode === "code") codeEl.style.display = "";
|
|
else if (mdEl) mdEl.style.display = "";
|
|
showToast(t("saved"));
|
|
} catch (e) {
|
|
setStatus(t("save_failed") + (e instanceof Error ? e.message : String(e)));
|
|
}
|
|
} else {
|
|
const currentText = _previewCurrentMode === "code" ? $("previewCode")?.textContent || "" : _previewRawContent || "";
|
|
if (editEl) {
|
|
editEl.value = currentText;
|
|
editEl.style.display = "";
|
|
}
|
|
const codeEl = $("previewCode");
|
|
const mdEl = $("previewMd");
|
|
if (codeEl && _previewCurrentMode === "code") codeEl.style.display = "none";
|
|
else if (mdEl) mdEl.style.display = "none";
|
|
editEl.onkeydown = (e) => {
|
|
if (e.key === "Escape") {
|
|
e.preventDefault();
|
|
cancelEditMode();
|
|
}
|
|
};
|
|
}
|
|
updateEditBtn();
|
|
}
|
|
function cancelEditMode() {
|
|
const editEl = $("previewEditArea");
|
|
if (editEl) {
|
|
editEl.style.display = "none";
|
|
editEl.onkeydown = null;
|
|
}
|
|
const codeEl = $("previewCode");
|
|
const mdEl = $("previewMd");
|
|
if (codeEl && _previewCurrentMode === "code") codeEl.style.display = "";
|
|
else if (mdEl) mdEl.style.display = "";
|
|
_previewDirty = false;
|
|
updateEditBtn();
|
|
}
|
|
async function openFile(path) {
|
|
if (!S.session) return;
|
|
const ext = fileExt(path);
|
|
if (DOWNLOAD_EXTS.has(ext)) {
|
|
downloadFile(path);
|
|
return;
|
|
}
|
|
const previewPathText = $("previewPathText");
|
|
const previewArea = $("previewArea");
|
|
const fileTree = $("fileTree");
|
|
const wsSearch = $("wsSearchWrap");
|
|
if (previewPathText) previewPathText.textContent = path;
|
|
previewArea?.classList.add("visible");
|
|
if (fileTree) fileTree.style.display = "none";
|
|
if (wsSearch) wsSearch.style.display = "none";
|
|
_previewCurrentPath = path;
|
|
renderFileBreadcrumb(path);
|
|
if (IMAGE_EXTS.has(ext)) {
|
|
showPreview("image");
|
|
const url = `api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`;
|
|
const previewImg = $("previewImg");
|
|
if (previewImg) {
|
|
previewImg.alt = path;
|
|
previewImg.src = url;
|
|
previewImg.onerror = () => setStatus(t("image_load_failed"));
|
|
}
|
|
} else if (MD_EXTS.has(ext)) {
|
|
try {
|
|
const data = await api(`/api/file?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
|
|
showPreview("md");
|
|
_previewRawContent = data.content || "";
|
|
const mdEl = $("previewMd");
|
|
if (mdEl) mdEl.innerHTML = renderMd(data.content || "");
|
|
requestAnimationFrame(() => {
|
|
if (typeof renderKatexBlocks === "function") renderKatexBlocks();
|
|
});
|
|
} catch (e) {
|
|
setStatus(t("file_open_failed"));
|
|
}
|
|
} else {
|
|
try {
|
|
const data = await api(`/api/file?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}`);
|
|
if (data.binary) {
|
|
downloadFile(path);
|
|
return;
|
|
}
|
|
showPreview("code");
|
|
const content = data.content || "";
|
|
const codeEl = $("previewCode");
|
|
if (["yml", "yaml"].includes(ext)) {
|
|
if (codeEl) {
|
|
codeEl.className = "preview-code hl-yaml";
|
|
codeEl.innerHTML = typeof highlightYAML === "function" ? highlightYAML(content) : _highlightWithLineNumbers(content);
|
|
}
|
|
} else if (ext === "json") {
|
|
if (codeEl) {
|
|
codeEl.className = "preview-code hl-json";
|
|
codeEl.innerHTML = _highlightJSON(content);
|
|
}
|
|
} else if (["py", "js", "ts", "sh", "bash", "zsh", "rb", "go", "rs", "java", "c", "cpp", "h", "css", "scss", "html", "xml", "sql", "r", "lua", "pl", "php", "swift", "kt", "dart"].includes(ext)) {
|
|
if (codeEl) {
|
|
codeEl.className = "preview-code";
|
|
codeEl.textContent = content;
|
|
requestAnimationFrame(() => {
|
|
if (typeof highlightCode === "function") highlightCode();
|
|
});
|
|
}
|
|
} else {
|
|
if (codeEl) {
|
|
codeEl.className = "preview-code hl-text";
|
|
codeEl.innerHTML = _highlightWithLineNumbers(content);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
downloadFile(path);
|
|
}
|
|
}
|
|
}
|
|
function downloadFile(path) {
|
|
if (!S.session) return;
|
|
const url = `api/file/raw?session_id=${encodeURIComponent(S.session.session_id)}&path=${encodeURIComponent(path)}&download=1`;
|
|
const filename = path.split("/").pop() || path;
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
setTimeout(() => document.body.removeChild(a), 100);
|
|
showToast(t("downloading", filename), 2e3);
|
|
}
|
|
function renderFileBreadcrumb(filePath) {
|
|
const bar = $("breadcrumbBar");
|
|
if (!bar) return;
|
|
bar.style.display = "flex";
|
|
const upBtn = $("btnUpDir");
|
|
if (upBtn) upBtn.style.display = "";
|
|
bar.innerHTML = "";
|
|
const root = document.createElement("span");
|
|
root.className = "breadcrumb-seg breadcrumb-link";
|
|
root.textContent = "~";
|
|
root.onclick = () => {
|
|
clearPreview();
|
|
loadDir(".");
|
|
};
|
|
bar.appendChild(root);
|
|
const parts = filePath.split("/");
|
|
let accumulated = "";
|
|
for (let i = 0; i < parts.length; i++) {
|
|
const sep = document.createElement("span");
|
|
sep.className = "breadcrumb-sep";
|
|
sep.textContent = "/";
|
|
bar.appendChild(sep);
|
|
accumulated += (accumulated ? "/" : "") + parts[i];
|
|
const seg = document.createElement("span");
|
|
seg.textContent = parts[i];
|
|
if (i < parts.length - 1) {
|
|
seg.className = "breadcrumb-seg breadcrumb-link";
|
|
const target = accumulated;
|
|
seg.onclick = () => {
|
|
clearPreview();
|
|
loadDir(target);
|
|
};
|
|
} else {
|
|
seg.className = "breadcrumb-seg breadcrumb-current";
|
|
}
|
|
bar.appendChild(seg);
|
|
}
|
|
}
|
|
function _escHtml(s) {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
function _highlightWithLineNumbers(text) {
|
|
return text.split("\n").map((line) => '<span class="code-line">' + _escHtml(line) + "</span>").join("\n");
|
|
}
|
|
function _highlightJSON(text) {
|
|
try {
|
|
const pretty = JSON.stringify(JSON.parse(text), null, 2);
|
|
return pretty.split("\n").map((raw) => {
|
|
let line = _escHtml(raw);
|
|
line = line.replace(/^(\s*)(")([\w\s.\/\-_@:]+)(")(\s*:)/, '$1<span class="hl-key">$2$3$4</span><span class="hl-value">$5</span>');
|
|
line = line.replace(/(:\s*)("[^&]*?")/g, '$1<span class="hl-string">$2</span>');
|
|
line = line.replace(/:\s*(\d+\.?\d*)/g, ': <span class="hl-number">$1</span>');
|
|
line = line.replace(/:\s*(true|false|null)/g, ': <span class="hl-bool">$1</span>');
|
|
return '<span class="code-line">' + line + "</span>";
|
|
}).join("\n");
|
|
} catch {
|
|
return _highlightWithLineNumbers(text);
|
|
}
|
|
}
|
|
let _wsSearchTimer = null;
|
|
function filterWsFiles() {
|
|
clearTimeout(_wsSearchTimer ?? void 0);
|
|
_wsSearchTimer = setTimeout(_doWsSearch, 300);
|
|
}
|
|
async function _doWsSearch() {
|
|
const input = $("wsSearchInput");
|
|
const clearBtn = $("wsSearchClear");
|
|
const tree = $("fileTree");
|
|
if (!input || !tree) return;
|
|
const query = input.value.trim().toLowerCase();
|
|
if (clearBtn) clearBtn.classList.toggle("visible", query.length > 0);
|
|
const oldNoRes = tree.querySelector(".ws-no-results");
|
|
if (oldNoRes) oldNoRes.remove();
|
|
if (!query) {
|
|
if (typeof renderFileTree === "function") renderFileTree();
|
|
return;
|
|
}
|
|
if (!S.session || !S.session.workspace) return;
|
|
tree.innerHTML = '<div class="ws-no-results" style="opacity:.5">Suche...</div>';
|
|
try {
|
|
const data = await api(`/api/list?session_id=${encodeURIComponent(S.session.session_id)}&path=.&search=${encodeURIComponent(query)}`);
|
|
const results = data.entries || [];
|
|
if (!results.length) {
|
|
tree.innerHTML = '<div class="ws-no-results">Keine Dateien gefunden</div>';
|
|
return;
|
|
}
|
|
tree.innerHTML = "";
|
|
for (const item of results) {
|
|
const el = document.createElement("div");
|
|
el.className = "file-item";
|
|
el.style.paddingLeft = "10px";
|
|
const iconEl = document.createElement("span");
|
|
iconEl.className = "file-icon";
|
|
iconEl.innerHTML = fileIcon(item.name, item.type);
|
|
el.appendChild(iconEl);
|
|
const nameEl = document.createElement("span");
|
|
nameEl.className = "file-name";
|
|
nameEl.textContent = item.name;
|
|
el.appendChild(nameEl);
|
|
if (item.path) {
|
|
const pathEl = document.createElement("span");
|
|
pathEl.className = "file-size";
|
|
pathEl.style.opacity = ".4";
|
|
const dir = item.path.substring(0, item.path.lastIndexOf("/"));
|
|
pathEl.textContent = dir || ".";
|
|
el.appendChild(pathEl);
|
|
}
|
|
if (item.type === "dir") el.onclick = () => {
|
|
clearWsSearch();
|
|
loadDir(item.path);
|
|
};
|
|
else el.onclick = () => openFile(item.path);
|
|
tree.appendChild(el);
|
|
}
|
|
} catch {
|
|
tree.innerHTML = "";
|
|
const allItems = S.entries || [];
|
|
const matches = allItems.filter((it) => it.name.toLowerCase().includes(query));
|
|
if (!matches.length) tree.innerHTML = '<div class="ws-no-results">Keine Dateien gefunden</div>';
|
|
else {
|
|
for (const item of matches) {
|
|
const el = document.createElement("div");
|
|
el.className = "file-item";
|
|
el.style.paddingLeft = "10px";
|
|
el.innerHTML = '<span class="file-icon">' + fileIcon(item.name, item.type) + '</span><span class="file-name">' + item.name + "</span>";
|
|
el.onclick = item.type === "dir" ? () => {
|
|
clearWsSearch();
|
|
loadDir(item.path);
|
|
} : () => openFile(item.path);
|
|
tree.appendChild(el);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function toggleWsSearch() {
|
|
const wrap = $("wsSearchWrap");
|
|
if (!wrap) return;
|
|
wrap.style.display = wrap.style.display === "none" ? "flex" : "none";
|
|
setTimeout(() => {
|
|
if (wrap.style.display !== "none") {
|
|
const inp = $("wsSearchInput");
|
|
if (inp) inp.focus();
|
|
}
|
|
}, 50);
|
|
}
|
|
function clearWsSearch() {
|
|
const input = $("wsSearchInput");
|
|
if (input) input.value = "";
|
|
const clearBtn = $("wsSearchClear");
|
|
if (clearBtn) clearBtn.classList.remove("visible");
|
|
if (typeof renderFileTree === "function") renderFileTree();
|
|
}
|
|
function refreshWorkspace() {
|
|
loadDir(".");
|
|
}
|
|
// ── Export to window for cross-module access ──
|
|
window.loadDir = loadDir;
|
|
window.renderFileTree = renderFileTree;
|
|
window.toggleWsSearch = toggleWsSearch;
|
|
window.clearWsSearch = clearWsSearch;
|
|
window.openFile = openFile;
|
|
window.showPreview = showPreview;
|
|
window.refreshWorkspace = refreshWorkspace;
|
|
})();
|
|
//# sourceMappingURL=workspace.js.map
|