CH-J Server Managerδιαχείριση διακομιστή πάνω από SSH
Μενού
Δημοσίευση πηγής

CH-J Server Manager

Περιηγηθείτε σε καταλόγους και αρχεία για μια συγκεκριμένη έκδοση εφαρμογών.

Κατεβάστε την πηγή ZIP
CH-J / Server Manager 339 αρχεία
core-20261014T184258Z-3fbed0b93fdf Προστέθηκε: Ημερομηνία που δεν έχει καταγραφεί · Χτισμένο:
Αρχεία
index.js Προστέθηκε: Ημερομηνία που δεν έχει καταγραφεί 21,9 KB · 186 γραμμές
Λήψη αρχείου
1"use strict";
3const api = window.chjPlugin;
4const byId = (id) => document.getElementById(id);
5const elements = Object.fromEntries([
6 "pluginVersion", "closeButton", "sessionSelect", "refreshSessionsButton", "rootButton", "upButton", "refreshButton", "breadcrumbs", "pathForm", "pathInput",
7 "newFileButton", "mkdirButton", "uploadButton", "downloadButton", "zipButton", "tarGzButton", "tarButton", "filterInput", "selectAllCheckbox", "filesBody",
8 "busyOverlay", "busyLabel", "status", "selectionInfo", "editor", "editorPath", "editorLanguage", "editorSize", "editorDirty", "editorHost", "editorPosition",
9 "downloadEditorButton", "saveEditorButton", "closeEditorButton", "contextMenu"
10].map((id) => [id, byId(id)]));
12const state = {
13 sessions: [], currentPath: "/", entries: [], visibleEntries: [], selected: new Set(), anchorPath: null,
14 sortKey: "name", sortDirection: 1, busy: false, monaco: null, editor: null, editorPath: null, savedText: "", monacoPromise: null
15};
17window.MonacoEnvironment = { getWorkerUrl: () => "vendor/monaco/vs/base/worker/workerMain.js" };
19function normalizePath(value) {
20 const raw = String(value || "").trim();
21 if (!raw.startsWith("/")) throw new Error("Cesta musí začínat znakem /.");
22 const parts = [];
23 for (const part of raw.split("/")) { if (!part || part === ".") continue; if (part === "..") parts.pop(); else parts.push(part); }
24 return `/${parts.join("/")}`;
26function joinPath(base, name) { return normalizePath(`${base === "/" ? "" : base}/${name}`); }
27function parentPath(value) { const parts = normalizePath(value).split("/").filter(Boolean); parts.pop(); return `/${parts.join("/")}`; }
28function selectedSessionId() { return elements.sessionSelect.value; }
29function selectedEntries() { return state.entries.filter((entry) => state.selected.has(entry.path)); }
30function bytes(value) { const units = ["B", "KiB", "MiB", "GiB", "TiB"]; let amount = Number(value || 0); let unit = 0; while (amount >= 1024 && unit < units.length - 1) { amount /= 1024; unit += 1; } return `${amount.toLocaleString("cs-CZ", { maximumFractionDigits: 1 })} ${units[unit]}`; }
31function setStatus(message, error = false) { elements.status.textContent = message; elements.status.classList.toggle("error", error); }
32function validName(value) { return Boolean(value && !/[\/\0\r\n]/.test(value) && value !== "." && value !== ".."); }
34function setBusy(value, label = "Pracuji…") {
35 state.busy = value; elements.busyOverlay.hidden = !value; elements.busyLabel.textContent = label;
36 for (const element of [elements.sessionSelect, elements.refreshSessionsButton, elements.rootButton, elements.upButton, elements.refreshButton, elements.newFileButton, elements.mkdirButton, elements.uploadButton]) element.disabled = value || (!selectedSessionId() && element !== elements.sessionSelect && element !== elements.refreshSessionsButton);
37 updateActions();
40function updateActions() {
41 const selected = selectedEntries(); const enabled = Boolean(selectedSessionId()) && !state.busy;
42 elements.downloadButton.disabled = !enabled || !selected.length;
43 for (const button of [elements.zipButton, elements.tarGzButton, elements.tarButton]) button.disabled = !enabled || !selected.length;
44 elements.selectAllCheckbox.checked = Boolean(state.visibleEntries.length) && state.visibleEntries.every((entry) => state.selected.has(entry.path));
45 elements.selectAllCheckbox.indeterminate = state.visibleEntries.some((entry) => state.selected.has(entry.path)) && !elements.selectAllCheckbox.checked;
46 elements.selectionInfo.textContent = `${state.visibleEntries.length} položek · ${selected.length} vybráno${selected.length ? ` · ${bytes(selected.reduce((sum, entry) => sum + (entry.type === "directory" ? 0 : entry.size), 0))}` : ""}`;
49function renderBreadcrumbs() {
50 const fragment = document.createDocumentFragment(); let current = "";
51 const append = (label, target) => { const button = document.createElement("button"); button.type = "button"; button.className = "crumb"; button.textContent = label; button.addEventListener("click", () => loadDirectory(target)); fragment.append(button); };
52 append("/", "/");
53 for (const part of state.currentPath.split("/").filter(Boolean)) { const separator = document.createElement("span"); separator.className = "crumb-sep"; separator.textContent = "›"; fragment.append(separator); current += `/${part}`; append(part, current); }
54 elements.breadcrumbs.replaceChildren(fragment); elements.breadcrumbs.scrollLeft = elements.breadcrumbs.scrollWidth;
57function compareEntries(left, right) {
58 if (left.type === "directory" && right.type !== "directory") return -1;
59 if (right.type === "directory" && left.type !== "directory") return 1;
60 const key = state.sortKey; let result;
61 if (key === "size") result = left.size - right.size;
62 else if (key === "modifiedAt") result = Date.parse(left.modifiedAt || 0) - Date.parse(right.modifiedAt || 0);
63 else result = String(left[key] || "").localeCompare(String(right[key] || ""), "cs", { numeric: true, sensitivity: "base" });
64 return result * state.sortDirection;
67function renderEntries() {
68 const query = elements.filterInput.value.trim().toLocaleLowerCase("cs");
69 state.visibleEntries = state.entries.filter((entry) => !query || entry.name.toLocaleLowerCase("cs").includes(query)).sort(compareEntries);
70 const fragment = document.createDocumentFragment();
71 for (const item of state.visibleEntries) {
72 const row = document.createElement("tr"); row.dataset.path = item.path; row.dataset.type = item.type; row.classList.toggle("selected", state.selected.has(item.path));
73 const checkCell = document.createElement("td"); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = state.selected.has(item.path); checkbox.dataset.select = item.path; checkCell.append(checkbox); row.append(checkCell);
74 const name = document.createElement("td"); name.className = "file-name"; const icon = document.createElement("span"); icon.className = "file-icon"; icon.textContent = item.type === "directory" ? "📁" : item.type === "symlink" ? "↗" : "📄"; name.append(icon, document.createTextNode(item.name)); row.append(name);
75 for (const value of [item.type === "directory" ? "složka" : item.type === "symlink" ? "odkaz" : (item.name.split(".").pop()?.toUpperCase() || "soubor"), item.type === "directory" ? "—" : bytes(item.size), item.modifiedAt ? new Date(item.modifiedAt).toLocaleString("cs-CZ") : "—"]) { const cell = document.createElement("td"); cell.className = "file-meta"; cell.textContent = value; row.append(cell); }
76 const actions = document.createElement("td"); const wrap = document.createElement("div"); wrap.className = "row-actions"; for (const [label, action] of [[item.type === "directory" ? "Otevřít" : "Upravit", "open"], ["⋯", "menu"]]) { const button = document.createElement("button"); button.type = "button"; button.className = "row-action"; button.textContent = label; button.dataset.rowAction = action; button.dataset.path = item.path; wrap.append(button); } actions.append(wrap); row.append(actions); fragment.append(row);
77 }
78 if (!state.visibleEntries.length) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = 6; cell.className = "empty"; cell.textContent = query ? "Filtru neodpovídá žádná položka." : "Adresář je prázdný."; row.append(cell); fragment.append(row); }
79 elements.filesBody.replaceChildren(fragment); updateActions();
82function selectPath(remotePath, options = {}) {
83 const index = state.visibleEntries.findIndex((entry) => entry.path === remotePath);
84 if (options.range && state.anchorPath) {
85 const anchor = state.visibleEntries.findIndex((entry) => entry.path === state.anchorPath); if (anchor >= 0 && index >= 0) for (let position = Math.min(anchor, index); position <= Math.max(anchor, index); position += 1) state.selected.add(state.visibleEntries[position].path);
86 } else if (options.toggle) { if (state.selected.has(remotePath)) state.selected.delete(remotePath); else state.selected.add(remotePath); state.anchorPath = remotePath; }
87 else { state.selected = new Set([remotePath]); state.anchorPath = remotePath; }
88 renderEntries();
91async function loadSessions() {
92 const previous = selectedSessionId(); state.sessions = (await api.sessions.list()).filter((session) => session.state === "connected");
93 elements.sessionSelect.replaceChildren(new Option(state.sessions.length ? "Vyberte relaci…" : "Žádná připojená relace", ""));
94 for (const session of state.sessions) elements.sessionSelect.add(new Option(`${session.label} — ${session.username}@${session.host}`, session.sessionId));
95 elements.sessionSelect.value = state.sessions.some((session) => session.sessionId === previous) ? previous : (state.sessions[0]?.sessionId || ""); setBusy(false);
98async function loadDirectory(requested = state.currentPath) {
99 if (!selectedSessionId() || state.busy) return; const target = normalizePath(requested); setBusy(true, `Načítám ${target}…`); hideContextMenu();
100 try { const entries = await api.files.list(selectedSessionId(), target); state.currentPath = target; state.entries = entries; state.selected.clear(); state.anchorPath = null; elements.pathInput.value = target; renderBreadcrumbs(); renderEntries(); setStatus(`Načteno ${entries.length} položek z ${target}.`); }
101 catch (error) { setStatus(error?.message || String(error), true); }
102 finally { setBusy(false); }
105async function mutate(label, action) {
106 if (state.busy) return; setBusy(true, label);
107 try { const result = await action(); if (result?.canceled) { setStatus("Operace byla zrušena."); return; } setBusy(false); await loadDirectory(state.currentPath); }
108 catch (error) { setStatus(error?.message || String(error), true); }
109 finally { setBusy(false); }
112function languageFor(remotePath) {
113 const extension = remotePath.toLowerCase().split(".").pop();
114 return ({ js: "javascript", mjs: "javascript", cjs: "javascript", ts: "typescript", tsx: "typescript", json: "json", html: "html", htm: "html", css: "css", scss: "scss", less: "less", md: "markdown", sh: "shell", bash: "shell", zsh: "shell", py: "python", go: "go", java: "java", c: "c", h: "c", cpp: "cpp", hpp: "cpp", rs: "rust", php: "php", sql: "sql", xml: "xml", yaml: "yaml", yml: "yaml", ini: "ini", toml: "ini", dockerfile: "dockerfile" })[extension] || (remotePath.toLowerCase().endsWith("dockerfile") ? "dockerfile" : "plaintext");
117function ensureMonaco() {
118 if (state.monaco) return Promise.resolve(state.monaco); if (state.monacoPromise) return state.monacoPromise;
119 state.monacoPromise = new Promise((resolve, reject) => { const amd = window.require; if (!amd?.config) return reject(new Error("Monaco AMD loader není dostupný.")); amd.config({ paths: { vs: "vendor/monaco/vs" }, preferScriptTags: true }); amd(["vs/editor/editor.main"], () => { if (!window.monaco?.editor) return reject(new Error("Monaco Editor se nepodařilo inicializovat.")); state.monaco = window.monaco; resolve(window.monaco); }, reject); }).catch((error) => { state.monacoPromise = null; throw error; });
120 return state.monacoPromise;
123function updateDirtyState() { const dirty = Boolean(state.editor && state.editor.getValue() !== state.savedText); elements.editorDirty.hidden = !dirty; return dirty; }
124async function saveEditor() {
125 if (!state.editorPath || !state.editor || state.busy) return; setBusy(true, "Ukládám soubor…");
126 try { const text = state.editor.getValue(); const result = await api.files.writeText(selectedSessionId(), state.editorPath, text); state.savedText = text; elements.editorSize.textContent = bytes(result.size); updateDirtyState(); setStatus(`Atomicky uloženo ${bytes(result.size)}.`); }
127 catch (error) { setStatus(error?.message || String(error), true); }
128 finally { setBusy(false); }
130async function openText(remotePath) {
131 if (state.busy) return; setBusy(true, `Otevírám ${remotePath}…`);
132 try {
133 const [result, monaco] = await Promise.all([api.files.readText(selectedSessionId(), remotePath), ensureMonaco()]); const language = languageFor(remotePath);
134 if (!state.editor) {
135 state.editor = monaco.editor.create(elements.editorHost, { value: result.text, language, theme: "vs-dark", automaticLayout: true, fontSize: 13, lineHeight: 20, minimap: { enabled: true }, smoothScrolling: true, bracketPairColorization: { enabled: true }, guides: { bracketPairs: true }, renderWhitespace: "selection", formatOnPaste: true, scrollBeyondLastLine: false });
136 state.editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, () => void saveEditor());
137 state.editor.onDidChangeModelContent(updateDirtyState); state.editor.onDidChangeCursorPosition((event) => { elements.editorPosition.textContent = `Řádek ${event.position.lineNumber}, sloupec ${event.position.column}`; });
138 } else { state.editor.setValue(result.text); monaco.editor.setModelLanguage(state.editor.getModel(), language); }
139 state.editorPath = result.path; state.savedText = result.text; elements.editorPath.textContent = result.path; elements.editorLanguage.textContent = language; elements.editorSize.textContent = bytes(result.size); updateDirtyState(); elements.editor.hidden = false; state.editor.focus(); setStatus(`Otevřen ${remotePath}.`);
140 } catch (error) { setStatus(error?.message || String(error), true); }
141 finally { setBusy(false); }
143function closeEditor() { if (!elements.editor.hidden && updateDirtyState() && !window.confirm("Soubor obsahuje neuložené změny. Opravdu zavřít editor?")) return; elements.editor.hidden = true; state.editorPath = null; state.savedText = ""; }
145async function openSelection() { const item = selectedEntries()[0]; if (!item) return; if (item.type === "directory") await loadDirectory(item.path); else await openText(item.path); }
146async function downloadSelection() {
147 const items = selectedEntries(); if (!items.length || state.busy) return; setBusy(true, `Stahuji ${items.length} položek…`);
148 try { const result = items.length === 1 && items[0].type !== "directory" ? await api.files.download(selectedSessionId(), items[0].path) : await api.files.downloadMany(selectedSessionId(), items.map((item) => item.path)); if (!result.canceled) setStatus(`Staženo ${result.entries || 1} položek, ${bytes(result.size)}.`); }
149 catch (error) { setStatus(error?.message || String(error), true); }
150 finally { setBusy(false); }
152async function archiveSelection(format) {
153 const items = selectedEntries(); if (!items.length || state.busy) return; setBusy(true, `Vytvářím ${format.toUpperCase()} archiv…`);
154 try { const result = await api.files.downloadArchive(selectedSessionId(), items.map((item) => item.path), format); if (!result.canceled) setStatus(`Archiv ${format.toUpperCase()} uložen: ${bytes(result.size)}, ${result.entries} položek.`); }
155 catch (error) { setStatus(error?.message || String(error), true); }
156 finally { setBusy(false); }
158async function renameSelection() { const item = selectedEntries()[0]; if (!item || selectedEntries().length !== 1) return; const name = window.prompt("Nový název:", item.name); if (!validName(name) || name === item.name) return; await mutate("Přejmenovávám…", () => api.files.rename(selectedSessionId(), item.path, joinPath(parentPath(item.path), name))); }
159async function removeSelection() { const items = selectedEntries(); if (!items.length || !window.confirm(`Opravdu trvale smazat ${items.length} vybraných položek včetně obsahu vybraných složek?`)) return; await mutate("Mažu vybrané položky…", () => api.files.removeMany(selectedSessionId(), items.map((item) => item.path), true)); }
161function hideContextMenu() { elements.contextMenu.hidden = true; }
162function showContextMenu(event, remotePath) { event.preventDefault(); if (remotePath && !state.selected.has(remotePath)) selectPath(remotePath); const item = selectedEntries()[0]; elements.contextMenu.querySelector('[data-context="rename"]').disabled = state.selected.size !== 1; elements.contextMenu.querySelector('[data-context="open"]').disabled = state.selected.size !== 1; elements.contextMenu.hidden = false; const rect = elements.contextMenu.getBoundingClientRect(); elements.contextMenu.style.left = `${Math.max(6, Math.min(event.clientX, window.innerWidth - rect.width - 6))}px`; elements.contextMenu.style.top = `${Math.max(6, Math.min(event.clientY, window.innerHeight - rect.height - 6))}px`; }
164elements.closeButton.addEventListener("click", () => api.close());
165elements.refreshSessionsButton.addEventListener("click", async () => { await loadSessions(); await loadDirectory(state.currentPath); });
166elements.sessionSelect.addEventListener("change", () => { setBusy(false); void loadDirectory("/"); });
167elements.pathForm.addEventListener("submit", (event) => { event.preventDefault(); void loadDirectory(elements.pathInput.value); });
168elements.rootButton.addEventListener("click", () => loadDirectory("/")); elements.upButton.addEventListener("click", () => loadDirectory(parentPath(state.currentPath))); elements.refreshButton.addEventListener("click", () => loadDirectory(state.currentPath));
169elements.filterInput.addEventListener("input", renderEntries);
170elements.selectAllCheckbox.addEventListener("change", () => { for (const item of state.visibleEntries) { if (elements.selectAllCheckbox.checked) state.selected.add(item.path); else state.selected.delete(item.path); } renderEntries(); });
171for (const button of document.querySelectorAll("button.sort")) button.addEventListener("click", () => { const key = button.dataset.sort; if (state.sortKey === key) state.sortDirection *= -1; else { state.sortKey = key; state.sortDirection = 1; } for (const other of document.querySelectorAll("button.sort")) { other.classList.toggle("active", other === button); other.querySelector("span").textContent = other === button ? (state.sortDirection > 0 ? "↑" : "↓") : ""; } renderEntries(); });
172elements.newFileButton.addEventListener("click", () => { const name = window.prompt("Název nového souboru:", "novy-soubor.txt"); if (!validName(name)) return; void mutate("Vytvářím soubor…", () => api.files.createFile(selectedSessionId(), joinPath(state.currentPath, name))); });
173elements.mkdirButton.addEventListener("click", () => { const name = window.prompt("Název nové složky:"); if (!validName(name)) return; void mutate("Vytvářím složku…", () => api.files.mkdir(selectedSessionId(), joinPath(state.currentPath, name))); });
174elements.uploadButton.addEventListener("click", () => void mutate("Nahrávám soubory a složky…", () => api.files.upload(selectedSessionId(), state.currentPath)));
175elements.downloadButton.addEventListener("click", () => void downloadSelection()); elements.zipButton.addEventListener("click", () => archiveSelection("zip")); elements.tarGzButton.addEventListener("click", () => archiveSelection("tar.gz")); elements.tarButton.addEventListener("click", () => archiveSelection("tar"));
176elements.filesBody.addEventListener("click", (event) => { const action = event.target.closest("button[data-row-action]"); if (action) { selectPath(action.dataset.path); if (action.dataset.rowAction === "open") void openSelection(); else showContextMenu(event, action.dataset.path); return; } const row = event.target.closest("tr[data-path]"); if (!row) return; selectPath(row.dataset.path, { toggle: event.metaKey || event.ctrlKey || event.target.matches("input[type=checkbox]"), range: event.shiftKey }); });
177elements.filesBody.addEventListener("dblclick", (event) => { const row = event.target.closest("tr[data-path]"); if (!row) return; selectPath(row.dataset.path); void openSelection(); }); elements.filesBody.addEventListener("contextmenu", (event) => { const row = event.target.closest("tr[data-path]"); showContextMenu(event, row?.dataset.path); });
178elements.contextMenu.addEventListener("click", (event) => { const action = event.target.closest("button[data-context]")?.dataset.context; hideContextMenu(); if (action === "open") void openSelection(); if (action === "download") void downloadSelection(); if (action === "rename") void renameSelection(); if (action === "remove") void removeSelection(); if (["zip", "tar", "tar.gz"].includes(action)) void archiveSelection(action); }); document.addEventListener("click", (event) => { if (!event.target.closest("#contextMenu") && !event.target.closest('[data-row-action="menu"]')) hideContextMenu(); });
179elements.saveEditorButton.addEventListener("click", () => void saveEditor()); elements.closeEditorButton.addEventListener("click", closeEditor); elements.downloadEditorButton.addEventListener("click", () => { if (state.editorPath) void api.files.download(selectedSessionId(), state.editorPath).catch((error) => setStatus(error?.message || String(error), true)); });
180window.addEventListener("keydown", (event) => { if (event.key === "Escape") { if (!elements.contextMenu.hidden) hideContextMenu(); else if (!elements.editor.hidden) closeEditor(); return; } if (!elements.editor.hidden || event.target.matches("input,select,textarea")) return; if (event.key === "F5") { event.preventDefault(); void loadDirectory(state.currentPath); } if (event.key === "F2") { event.preventDefault(); void renameSelection(); } if (event.key === "Delete") { event.preventDefault(); void removeSelection(); } if (event.key === "Enter") { event.preventDefault(); void openSelection(); } if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "a") { event.preventDefault(); state.selected = new Set(state.visibleEntries.map((entry) => entry.path)); renderEntries(); } if (event.key === "Backspace") { event.preventDefault(); void loadDirectory(parentPath(state.currentPath)); } });
182(async () => {
183 const info = await api.getInfo(); elements.pluginVersion.textContent = `${info.name} v${info.version} · ${info.publisher}`;
184 if (!api.files?.downloadArchive || !api.files?.downloadMany || !api.files?.createFile || !api.files?.removeMany) { for (const element of document.querySelectorAll("main button, main input, main select")) element.disabled = true; setStatus("Tento build pluginu vyžaduje nový Core s rozšířeným files.* API. Nejdříve aktualizujte aplikaci.", true); return; }
185 await loadSessions(); await loadDirectory("/");
186})().catch((error) => setStatus(error?.message || String(error), true));
SHA-2566e7cf5144c3bb8633706a97ca2f860df5094f65e32898493b6bff1b50af6856f

CH-J Proprietary Software License 1.14 · Η πηγή παρέχεται στο CH-J Proprietary Software License 1.14. Η διαθεσιμότητά του δεν αλλάζει τους όρους της άδειας χρήσης ούτε χορηγεί πρόσθετα δικαιώματα.

Archive SHA-2563df08cf5e899262a6ce92d7ec2caf5c42b683f1f2b57055662b845ff7dde67a8