Julkaistu lähde
Lataa lähde ZIP
CH-J Server Manager
Selaile hakemistoja ja tiedostoja tietyn sovelluksen julkaisua varten.
CH-J / Server Manager
339 tiedostot
core-20261014T184258Z-3fbed0b93fdf
Lisätty: Kirjaamaton päivä · Rakennettu:
1
"use strict";3
const api = window.chjPlugin;4
const byId = (id) => document.getElementById(id);5
const 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)]));12
const 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: null15
};17
window.MonacoEnvironment = { getWorkerUrl: () => "vendor/monaco/vs/base/worker/workerMain.js" };19
function 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("/")}`;25
}26
function joinPath(base, name) { return normalizePath(`${base === "/" ? "" : base}/${name}`); }27
function parentPath(value) { const parts = normalizePath(value).split("/").filter(Boolean); parts.pop(); return `/${parts.join("/")}`; }28
function selectedSessionId() { return elements.sessionSelect.value; }29
function selectedEntries() { return state.entries.filter((entry) => state.selected.has(entry.path)); }30
function 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]}`; }31
function setStatus(message, error = false) { elements.status.textContent = message; elements.status.classList.toggle("error", error); }32
function validName(value) { return Boolean(value && !/[\/\0\r\n]/.test(value) && value !== "." && value !== ".."); }34
function 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();38
}40
function 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))}` : ""}`;47
}49
function 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;55
}57
function 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;65
}67
function 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();80
}82
function 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();89
}91
async 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);96
}98
async 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); }103
}105
async 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); }110
}112
function 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");115
}117
function 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;121
}123
function updateDirtyState() { const dirty = Boolean(state.editor && state.editor.getValue() !== state.savedText); elements.editorDirty.hidden = !dirty; return dirty; }124
async 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); }129
}130
async 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); }142
}143
function 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 = ""; }145
async function openSelection() { const item = selectedEntries()[0]; if (!item) return; if (item.type === "directory") await loadDirectory(item.path); else await openText(item.path); }146
async 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); }151
}152
async 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); }157
}158
async 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))); }159
async 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)); }161
function hideContextMenu() { elements.contextMenu.hidden = true; }162
function 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`; }164
elements.closeButton.addEventListener("click", () => api.close());165
elements.refreshSessionsButton.addEventListener("click", async () => { await loadSessions(); await loadDirectory(state.currentPath); });166
elements.sessionSelect.addEventListener("change", () => { setBusy(false); void loadDirectory("/"); });167
elements.pathForm.addEventListener("submit", (event) => { event.preventDefault(); void loadDirectory(elements.pathInput.value); });168
elements.rootButton.addEventListener("click", () => loadDirectory("/")); elements.upButton.addEventListener("click", () => loadDirectory(parentPath(state.currentPath))); elements.refreshButton.addEventListener("click", () => loadDirectory(state.currentPath));169
elements.filterInput.addEventListener("input", renderEntries);170
elements.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(); });171
for (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(); });172
elements.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))); });173
elements.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))); });174
elements.uploadButton.addEventListener("click", () => void mutate("Nahrávám soubory a složky…", () => api.files.upload(selectedSessionId(), state.currentPath)));175
elements.downloadButton.addEventListener("click", () => void downloadSelection()); elements.zipButton.addEventListener("click", () => archiveSelection("zip")); elements.tarGzButton.addEventListener("click", () => archiveSelection("tar.gz")); elements.tarButton.addEventListener("click", () => archiveSelection("tar"));176
elements.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 }); });177
elements.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); });178
elements.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(); });179
elements.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)); });180
window.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-256
6e7cf5144c3bb8633706a97ca2f860df5094f65e32898493b6bff1b50af6856fCH-J Proprietary Software License 1.14 · Lähde ilmoitetaan CH-J Proprietary Software License 1.14-koodilla. Sen saatavuus ei muuta lisenssiehtoja eikä myönnä lisäoikeuksia.
Arkisto SHA-256
3df08cf5e899262a6ce92d7ec2caf5c42b683f1f2b57055662b845ff7dde67a8