CH-J Toolsspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Tools

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / Server Manager 872 souborů
core-20261030T220507Z-61e4558d889f Přidáno: · Sestaveno:
panels.js Přidáno: Datum není evidováno 26,0 KB · 416 řádků
Kód Stáhnout soubor
1"use strict";
3// Panels of the File Manager: local disk or any open SSH session, one to three side by side.
4// Copies always go through the Transfer Manager; the classic one-panel view keeps the editor, sudo and archives.
5(function () {
6 const local = api.files.local;
7 const LOCAL = "local";
8 const $ = (id) => document.getElementById(id);
9 const view = { mode: 3, panels: [], active: null, clipboard: null, places: null, drag: null, refreshTimer: null };
10 try { const saved = Number(localStorage.getItem("chj.fm.panels")); if ([1, 2, 3].includes(saved)) view.mode = saved; } catch {}
12 const isLocal = (panel) => panel.source === LOCAL;
13 const sep = () => view.places?.separator || "/";
14 const session = (id) => state.sessions.find((entry) => entry.sessionId === id);
15 const panelName = (panel) => isLocal(panel) ? t("Lokální disk") : session(panel.source) ? api.sessions.displayName(session(panel.source)) : "—";
16 const isDir = (item) => item.type === "directory" || item.targetType === "directory";
17 const visible = () => view.panels.slice(0, view.mode);
19 function joinFor(panel, name) {
20 if (!isLocal(panel)) return joinPath(panel.path, name);
21 return `${panel.path.replace(/[\\/]+$/, "")}${sep()}${name}`;
22 }
23 function parentFor(panel) {
24 if (!isLocal(panel)) return parentPath(panel.path);
25 const trimmed = panel.path.replace(/[\\/]+$/, "");
26 const cut = trimmed.lastIndexOf(sep());
27 if (cut <= 0) return view.places?.roots?.[0] || sep();
28 const parent = trimmed.slice(0, cut);
29 return /^[A-Za-z]:$/.test(parent) ? `${parent}\\` : parent;
30 }
31 function panelStatus(panel, message, error = false) { panel.statusNode.textContent = message; panel.statusNode.classList.toggle("error", error); }
33 function createPanel(index) {
34 const root = document.createElement("article");
35 root.className = "fs-panel panel";
36 root.innerHTML = `<header class="fs-head"><span class="fs-dot"></span><select class="fs-source" data-i18n-aria-label="Umístění panelu"></select></header>
37 <div class="fs-nav"><button type="button" class="icon-button" data-nav="back" data-i18n-title="Zpět">‹</button><button type="button" class="icon-button" data-nav="forward" data-i18n-title="Vpřed">›</button><button type="button" class="icon-button" data-nav="up" data-i18n-title="O úroveň výše">↑</button><button type="button" class="icon-button" data-nav="refresh" data-i18n-title="Obnovit (F5)">↻</button><form class="fs-path"><input maxlength="4096" spellcheck="false" autocomplete="off" data-i18n-aria-label="Cesta"></form></div>
38 <div class="fs-table"><table class="files-table"><thead><tr><th class="check-col"></th><th data-i18n="Název"></th><th data-i18n="Velikost"></th><th data-i18n="Změněno"></th></tr></thead><tbody></tbody></table></div>
39 <footer class="fs-status"></footer>`;
40 const panel = { index, root, source: "", path: "/", back: [], forward: [], entries: [], selected: new Set(), anchor: null, generation: 0, ended: false, touched: false,
41 select: root.querySelector(".fs-source"), input: root.querySelector(".fs-path input"), body: root.querySelector("tbody"), statusNode: root.querySelector(".fs-status"), dot: root.querySelector(".fs-dot") };
42 panel.select.addEventListener("change", () => { panel.touched = true; panel.ended = false; setSource(panel, panel.select.value); });
43 root.querySelector(".fs-path").addEventListener("submit", (event) => { event.preventDefault(); void open(panel, panel.input.value.trim()); });
44 root.querySelector(".fs-nav").addEventListener("click", (event) => {
45 const action = event.target.closest("[data-nav]")?.dataset.nav;
46 if (action === "up") void open(panel, parentFor(panel));
47 else if (action === "refresh") void reload(panel, true);
48 else if (action === "back" && panel.back.length) { panel.forward.push(panel.path); void open(panel, panel.back.pop(), false); }
49 else if (action === "forward" && panel.forward.length) { panel.back.push(panel.path); void open(panel, panel.forward.pop(), false); }
50 });
51 root.addEventListener("pointerdown", () => setActive(panel));
52 root.querySelector(".fs-table").addEventListener("contextmenu", (event) => { if (!event.target.closest("tbody")) { event.preventDefault(); setActive(panel); panel.selected = new Set(); showSelection(panel); showMenu(panel, event.clientX, event.clientY); } });
53 root.addEventListener("focusin", () => setActive(panel));
54 panel.body.addEventListener("click", (event) => {
55 const row = event.target.closest("tr[data-path]");
56 if (!row) return;
57 const path = row.dataset.path;
58 if (event.target.matches("input[type=checkbox]")) toggle(panel, path);
59 else if (event.shiftKey && panel.anchor) range(panel, path);
60 else if (event.metaKey || event.ctrlKey) toggle(panel, path);
61 else { panel.selected = new Set([path]); panel.anchor = path; }
62 // In place: replacing the rows between two clicks would lose the double click.
63 showSelection(panel);
64 });
65 panel.body.addEventListener("contextmenu", (event) => {
66 event.preventDefault();
67 setActive(panel);
68 const row = event.target.closest("tr[data-path]");
69 if (row && !panel.selected.has(row.dataset.path)) { panel.selected = new Set([row.dataset.path]); panel.anchor = row.dataset.path; showSelection(panel); }
70 if (!row) { panel.selected = new Set(); showSelection(panel); }
71 showMenu(panel, event.clientX, event.clientY);
72 });
73 panel.body.addEventListener("dblclick", (event) => {
74 const row = event.target.closest("tr[data-path]");
75 const item = row && panel.entries.find((entry) => entry.path === row.dataset.path);
76 if (!item) return;
77 if (isDir(item)) void open(panel, item.path);
78 else if (!isLocal(panel)) void openInEditor(panel, item.path);
79 });
80 panel.body.addEventListener("dragstart", (event) => {
81 const row = event.target.closest("tr[data-path]");
82 if (!row) return;
83 if (!panel.selected.has(row.dataset.path)) { panel.selected = new Set([row.dataset.path]); render(panel); }
84 view.drag = { panel, paths: [...panel.selected], directory: panel.path, source: panel.source };
85 event.dataTransfer.effectAllowed = "copyMove";
86 event.dataTransfer.setData("text/plain", [...panel.selected].join("\n"));
87 });
88 root.addEventListener("dragover", (event) => { if (view.drag && view.drag.panel !== panel && panel.source) { event.preventDefault(); root.classList.add("drop-target"); event.dataTransfer.dropEffect = event.shiftKey ? "move" : "copy"; } });
89 root.addEventListener("dragleave", (event) => { if (!root.contains(event.relatedTarget)) root.classList.remove("drop-target"); });
90 root.addEventListener("drop", (event) => {
91 root.classList.remove("drop-target");
92 if (!view.drag || view.drag.panel === panel) return;
93 event.preventDefault();
94 const drag = view.drag; view.drag = null;
95 void transfer(drag, panel, event.shiftKey);
96 });
97 root.addEventListener("dragend", () => { view.drag = null; for (const item of view.panels) item.root.classList.remove("drop-target"); });
98 return panel;
99 }
101 function toggle(panel, path) { if (panel.selected.has(path)) panel.selected.delete(path); else panel.selected.add(path); panel.anchor = path; }
102 function range(panel, path) {
103 const list = sorted(panel), from = list.findIndex((entry) => entry.path === panel.anchor), to = list.findIndex((entry) => entry.path === path);
104 if (from < 0 || to < 0) return;
105 for (let i = Math.min(from, to); i <= Math.max(from, to); i += 1) panel.selected.add(list[i].path);
106 }
107 function setActive(panel) {
108 if (view.active === panel) return;
109 view.active = panel;
110 for (const item of view.panels) item.root.classList.toggle("active", item === panel);
111 }
112 function sorted(panel) {
113 return [...panel.entries].sort((a, b) => (isDir(b) - isDir(a)) || String(a.name).localeCompare(String(b.name), i18n.locale(), { numeric: true, sensitivity: "base" }));
114 }
116 function render(panel) {
117 panel.input.value = panel.source ? panel.path : "";
118 panel.dot.dataset.kind = !panel.source ? "none" : isLocal(panel) ? "local" : "remote";
119 const fragment = document.createDocumentFragment();
120 for (const item of sorted(panel)) {
121 const row = document.createElement("tr");
122 row.dataset.path = item.path; row.draggable = true; row.classList.toggle("selected", panel.selected.has(item.path));
123 const checkCell = document.createElement("td"); const check = document.createElement("input"); check.type = "checkbox"; check.checked = panel.selected.has(item.path); check.setAttribute("aria-label", item.name); checkCell.append(check);
124 const name = document.createElement("td"); name.className = "file-name"; const icon = document.createElement("span"); icon.className = "file-icon"; icon.textContent = isDir(item) ? "📁" : item.type === "symlink" ? "↗" : "📄"; name.append(icon, document.createTextNode(item.name));
125 const size = document.createElement("td"); size.className = "file-meta"; size.textContent = isDir(item) ? "—" : bytes(item.size);
126 const when = document.createElement("td"); when.className = "file-meta"; when.textContent = item.modifiedAt ? new Date(item.modifiedAt).toLocaleString(i18n.locale()) : "—";
127 row.append(checkCell, name, size, when);
128 fragment.append(row);
129 }
130 if (!panel.entries.length) {
131 const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = 4; cell.className = "empty";
132 cell.textContent = panel.source ? t("Adresář je prázdný.") : t("Vyberte umístění panelu.");
133 row.append(cell); fragment.append(row);
134 }
135 panel.body.replaceChildren(fragment);
136 const chosen = panel.entries.filter((entry) => panel.selected.has(entry.path));
137 if (panel.source && !panel.statusNode.classList.contains("error")) panelStatus(panel, t("{count} položek · {selected} vybráno", { count: panel.entries.length, selected: chosen.length }));
138 updateActions();
139 }
141 function showSelection(panel) {
142 for (const row of panel.body.querySelectorAll("tr[data-path]")) {
143 const on = panel.selected.has(row.dataset.path);
144 row.classList.toggle("selected", on);
145 row.querySelector("input").checked = on;
146 }
147 if (panel.source && !panel.statusNode.classList.contains("error")) panelStatus(panel, t("{count} položek · {selected} vybráno", { count: panel.entries.length, selected: panel.selected.size }));
148 updateActions();
149 }
151 function hideMenu() { $("panelMenu").hidden = true; }
152 function showMenu(panel, x, y) {
153 const menu = $("panelMenu"), chosen = panel.entries.filter((entry) => panel.selected.has(entry.path)), one = chosen.length === 1 ? chosen[0] : null;
154 const add = (label, action, enabled = true, danger = false) => { const button = document.createElement("button"); button.type = "button"; button.textContent = label; button.disabled = !enabled; if (danger) button.className = "danger-text"; button.addEventListener("click", () => { hideMenu(); action(); }); menu.append(button); };
155 const line = () => { const separator = document.createElement("div"); separator.className = "menu-separator"; menu.append(separator); };
156 menu.replaceChildren();
157 if (one && isDir(one)) add(t("Otevřít"), () => void open(panel, one.path));
158 else if (one && !isLocal(panel)) add(t("Upravit"), () => void openInEditor(panel, one.path));
159 if (one) line();
160 if (chosen.length) {
161 add(t("Kopírovat"), () => toClipboard(false));
162 add(t("Vyjmout"), () => toClipboard(true));
163 }
164 add(t("Vložit"), paste, Boolean(view.clipboard && panel.source));
165 const others = visible().filter((other) => other !== panel && other.source && !(isLocal(other) && isLocal(panel)));
166 if (chosen.length && others.length) {
167 line();
168 for (const other of others) add(`${t("Kopírovat")} → ${panelName(other)}`, () => void transfer(selection(panel), other, false));
169 for (const other of others) add(`${t("Přesun")} → ${panelName(other)}`, () => void transfer(selection(panel), other, true));
170 }
171 line();
172 if (one) add(t("Přejmenovat"), () => void rename());
173 add(t("+ Soubor"), () => void newFile(), Boolean(panel.source));
174 add(t("+ Složka"), () => void mkdir(), Boolean(panel.source));
175 add(t("Obnovit (F5)"), () => void reload(panel, true), Boolean(panel.source));
176 if (chosen.length) { line(); add(t("Smazat"), () => void remove(), true, true); }
177 menu.hidden = false;
178 const box = menu.getBoundingClientRect();
179 menu.style.left = `${Math.max(4, Math.min(x, innerWidth - box.width - 4))}px`;
180 menu.style.top = `${Math.max(4, Math.min(y, innerHeight - box.height - 4))}px`;
181 menu.querySelector("button:not(:disabled)")?.focus();
182 }
184 function updateActions() {
185 const panel = view.active, chosen = panel ? panel.selected.size : 0;
186 for (const id of ["panelCopy", "panelCut", "panelDelete"]) $(id).disabled = !chosen;
187 $("panelRename").disabled = chosen !== 1;
188 $("panelMkdir").disabled = !panel?.source;
189 $("panelNewFile").disabled = !panel?.source;
190 $("panelPaste").disabled = !view.clipboard || !panel?.source;
191 $("panelClipboard").textContent = view.clipboard ? `${view.clipboard.cut ? t("Vyjmuto") : t("Zkopírováno")}: ${view.clipboard.paths.length}` : "";
192 }
194 async function open(panel, path, remember = true) {
195 if (!panel.source || !path) return;
196 const token = ++panel.generation, source = panel.source;
197 panelStatus(panel, t("Načítám {path}…", { path }));
198 try {
199 const target = isLocal(panel) ? path : normalizePath(path);
200 const entries = isLocal(panel) ? await local.list(target) : await api.files.list(source, target, {});
201 if (token !== panel.generation || source !== panel.source) return;
202 if (remember && target !== panel.path) { panel.back.push(panel.path); panel.forward = []; }
203 panel.path = target; panel.entries = entries; panel.selected = new Set(); panel.anchor = null;
204 panel.statusNode.classList.remove("error");
205 render(panel);
206 } catch (error) {
207 if (token !== panel.generation) return;
208 panel.input.value = panel.path;
209 panelStatus(panel, errorText(error), true);
210 }
211 }
212 async function reload(panel, clearError = false) {
213 if (!panel.source) return;
214 if (clearError) panel.statusNode.classList.remove("error");
215 const token = ++panel.generation, source = panel.source, keep = new Set(panel.selected);
216 try {
217 const entries = isLocal(panel) ? await local.list(panel.path) : await api.files.list(source, panel.path, {});
218 if (token !== panel.generation || source !== panel.source) return;
219 panel.entries = entries; panel.selected = new Set(entries.filter((entry) => keep.has(entry.path)).map((entry) => entry.path));
220 render(panel);
221 } catch (error) { if (token === panel.generation) panelStatus(panel, errorText(error), true); }
222 }
224 function setSource(panel, source) {
225 panel.generation += 1;
226 panel.source = source; panel.entries = []; panel.selected = new Set(); panel.anchor = null; panel.back = []; panel.forward = [];
227 panel.statusNode.classList.remove("error");
228 panel.path = source === LOCAL ? (view.places?.home || "/") : "/";
229 render(panel);
230 if (source) void open(panel, panel.path, false);
231 }
233 function renderSources(panel) {
234 const list = state.sessions;
235 panel.select.replaceChildren(new Option(list.length ? t("Vyberte relaci…") : t("Žádná připojená relace"), ""), new Option(t("Lokální disk"), LOCAL));
236 for (const entry of list) panel.select.add(new Option(api.sessions.displayName(entry), entry.sessionId));
237 panel.select.value = panel.source;
238 }
240 // One open session fills the next free panel. A session that ended never makes its panel jump to another
241 // server; with two sessions and one free panel the user decides.
242 function syncSessions(sessions) {
243 if (!view.panels.length) return;
244 const ids = new Set(sessions.map((entry) => entry.sessionId));
245 for (const panel of view.panels) {
246 if (panel.source && panel.source !== LOCAL && !ids.has(panel.source)) {
247 panel.ended = true;
248 setSource(panel, "");
249 panelStatus(panel, t("Relace skončila. Vyberte server."), true);
250 }
251 }
252 // T1 before T2: the same order every time.
253 const ordered = [...sessions].sort((a, b) => (a.slot || 99) - (b.slot || 99));
254 const free = () => ordered.filter((entry) => !view.panels.some((panel) => panel.source === entry.sessionId));
255 const empty = visible().filter((panel) => !panel.source && !panel.ended && !panel.touched);
256 if (active() && free().length <= empty.length) for (const panel of empty) { const next = free()[0]; if (next) setSource(panel, next.sessionId); }
257 for (const panel of view.panels) renderSources(panel);
258 if (active() && empty[0] && !empty[0].source && free().length > empty.length) api.sessions.offerChoice(empty[0].select, free(), t("Vyberte relaci…"));
259 updateActions();
260 }
262 function selection(panel) { return { panel, paths: [...panel.selected], directory: panel.path, source: panel.source }; }
264 async function transfer(from, to, move) {
265 if (!to.source || !from.source) return;
266 if (from.source === LOCAL && to.source === LOCAL) { panelStatus(to, t("Mezi dvěma lokálními panely se nekopíruje."), true); return; }
267 if (from.source !== LOCAL && !session(from.source)) { panelStatus(to, t("Zdrojová relace už není připojena."), true); return; }
268 const fromName = from.source === LOCAL ? t("Lokální disk") : api.sessions.displayName(session(from.source));
269 try {
270 // Within one connection a move is a single atomic rename.
271 if (move && from.source === to.source) {
272 for (const path of from.paths) await api.files.rename(from.source, path, joinPath(to.path, path.split("/").pop()), {});
273 for (const panel of visible()) void reload(panel);
274 if (view.clipboard?.cut) { view.clipboard = null; updateActions(); }
275 return;
276 }
277 panelStatus(to, t("Připravuji přenos…"));
278 const plan = await api.files.transfers.planCopy(
279 from.source === LOCAL ? { kind: "local", paths: from.paths } : { kind: "remote", sessionId: from.source, paths: from.paths },
280 to.source === LOCAL ? { kind: "local", directory: to.path } : { kind: "remote", sessionId: to.source, directory: to.path });
281 to.statusNode.classList.remove("error");
282 render(to);
283 if (plan.canceled) return;
284 if (!plan.files && !plan.directories) { panelStatus(to, t("Výběr neobsahuje žádné přenositelné soubory.")); return; }
285 plan.title = `${move ? t("Přesun") : t("Kopírování")}: ${fromName} → ${panelName(to)}`;
286 await runPlanned(plan, to, move, from.directory);
287 if (view.clipboard?.cut) { view.clipboard = null; updateActions(); }
288 } catch (error) { panelStatus(to, errorText(error), true); }
289 }
291 async function runPlanned(plan, target, move, sourceDirectory) {
292 let current = plan;
293 for (let attempt = 0; attempt < 4; attempt += 1) {
294 const choice = await showPlan(current);
295 if (!choice) return;
296 try {
297 const started = await api.files.transfers.start(plan.planId, { ...choice, ...(move ? { move: true, sourceDirectory } : {}) });
298 setStatus(`${t("Zařazeno {count} přenosů", { count: started.jobs })}${started.skipped ? `, ${t("přeskočeno {count}", { count: started.skipped })}` : ""}.`);
299 setTransfersCollapsed(false);
300 // Only empty folders: there are no jobs, and so no finished transfer to refresh after.
301 if (!started.jobs) for (const panel of visible()) void reload(panel);
302 return;
303 } catch (error) {
304 const code = errorCode(error);
305 if (code !== "SUDO_PASSWORD_INCORRECT" && code !== "SUDO_PASSWORD_REQUIRED") throw error;
306 current = { ...current, sudoPasswordRequired: true, error: errorText(error) };
307 }
308 }
309 }
311 async function openInEditor(panel, path) {
312 if (state.sessionId !== panel.source) {
313 elements.sessionSelect.value = panel.source;
314 elements.sessionSelect.dispatchEvent(new Event("change", { bubbles: true }));
315 if (state.sessionId !== panel.source) return;
316 for (let wait = 0; state.busy && wait < 100; wait += 1) await new Promise((done) => setTimeout(done, 50));
317 }
318 await openText(path);
319 }
321 async function mkdir() {
322 const panel = view.active; if (!panel?.source) return;
323 const name = await askName(t("Název nové složky:")); if (!name) return;
324 try { if (isLocal(panel)) await local.mkdir(panel.path, name); else await api.files.mkdir(panel.source, joinPath(panel.path, name), {}); await reload(panel, true); }
325 catch (error) { panelStatus(panel, errorText(error), true); }
326 }
327 // Any name and extension. On a server the new file opens in the editor right away.
328 async function newFile() {
329 const panel = view.active; if (!panel?.source) return;
330 const name = await askName(t("Název nového souboru:"), t("novy-soubor.txt")); if (!name) return;
331 try {
332 if (isLocal(panel)) { await local.createFile(panel.path, name); await reload(panel, true); return; }
333 const path = joinPath(panel.path, name);
334 await api.files.createFile(panel.source, path, {});
335 await reload(panel, true);
336 await openInEditor(panel, path);
337 } catch (error) { panelStatus(panel, errorText(error), true); }
338 }
339 async function rename() {
340 const panel = view.active; if (panel?.selected.size !== 1) return;
341 const path = [...panel.selected][0], item = panel.entries.find((entry) => entry.path === path);
342 const name = await askName(t("Nový název:"), item?.name); if (!name || name === item?.name) return;
343 try { if (isLocal(panel)) await local.rename(path, name); else await api.files.rename(panel.source, path, joinPath(panel.path, name), {}); await reload(panel, true); }
344 catch (error) { panelStatus(panel, errorText(error), true); }
345 }
346 async function remove() {
347 const panel = view.active; if (!panel?.selected.size) return;
348 if (!window.confirm(`${panelName(panel)}\n\n${t("Opravdu trvale smazat {count} vybraných položek včetně obsahu vybraných složek?", { count: panel.selected.size })}`)) return;
349 try { if (isLocal(panel)) await local.remove([...panel.selected]); else await api.files.removeMany(panel.source, [...panel.selected], true, {}); await reload(panel, true); }
350 catch (error) { panelStatus(panel, errorText(error), true); }
351 }
352 function toClipboard(cut) { const panel = view.active; if (!panel?.selected.size) return; view.clipboard = { ...selection(panel), cut }; updateActions(); }
353 function paste() { const panel = view.active; if (!panel?.source || !view.clipboard) return; void transfer(view.clipboard, panel, view.clipboard.cut); }
355 function active() { return view.mode > 1; }
356 function applyMode() {
357 document.querySelector("main").classList.toggle("panels-mode", active());
358 document.body.classList.toggle("panels-active", active());
359 $("panelsView").hidden = !active();
360 view.panels.forEach((panel, index) => { panel.root.hidden = index >= view.mode; });
361 document.querySelectorAll("#panelMode button").forEach((button) => { const on = Number(button.dataset.panels) === view.mode; button.classList.toggle("active", on); button.setAttribute("aria-checked", String(on)); });
362 $("panelGrid").style.setProperty("--panels", String(view.mode));
363 if (active()) { if (!view.active || view.panels.indexOf(view.active) >= view.mode) setActive(view.panels[0]); syncSessions(state.sessions); }
364 queueTransferRender();
365 }
367 // Refreshes open folders after finished transfers, at most every 1.5 s.
368 function transfersFinished() {
369 if (view.refreshTimer || !active()) return;
370 view.refreshTimer = setTimeout(() => { view.refreshTimer = null; for (const panel of visible()) void reload(panel); }, 1500);
371 }
373 document.addEventListener("pointerdown", (event) => { if (!event.target.closest("#panelMenu")) hideMenu(); });
374 window.addEventListener("blur", hideMenu);
375 $("panelMode").addEventListener("click", (event) => {
376 const mode = Number(event.target.closest("[data-panels]")?.dataset.panels);
377 if (!mode) return;
378 view.mode = mode;
379 try { localStorage.setItem("chj.fm.panels", String(mode)); } catch {}
380 applyMode();
381 });
382 $("panelCopy").addEventListener("click", () => toClipboard(false));
383 $("panelCut").addEventListener("click", () => toClipboard(true));
384 $("panelPaste").addEventListener("click", paste);
385 $("panelRename").addEventListener("click", () => void rename());
386 $("panelDelete").addEventListener("click", () => void remove());
387 $("panelMkdir").addEventListener("click", () => void mkdir());
388 $("panelNewFile").addEventListener("click", () => void newFile());
389 window.addEventListener("keydown", (event) => {
390 if (!active() || !elements.editor.hidden || document.querySelector("dialog[open]") || event.target.matches?.("input:not([type=checkbox]), select, textarea")) return;
391 if (event.key === "Escape") { hideMenu(); return; }
392 const mod = event.metaKey || event.ctrlKey, key = event.key.toLowerCase();
393 if (mod && key === "c") toClipboard(false);
394 else if (mod && key === "x") toClipboard(true);
395 else if (mod && key === "v") paste();
396 else if (mod && key === "a" && view.active) { view.active.selected = new Set(view.active.entries.map((entry) => entry.path)); render(view.active); }
397 else if (event.key === "F2") void rename();
398 else if (event.key === "F5" && view.active) void reload(view.active, true);
399 else if (event.key === "Delete" || (event.key === "Backspace" && event.metaKey)) void remove();
400 else return;
401 event.preventDefault(); event.stopPropagation();
402 }, true);
403 // This handler can run before the one in index.js, so the language is set here as well.
404 api.ui.onLanguageChanged((language) => { i18n.setLanguage(language); for (const panel of view.panels) { renderSources(panel); render(panel); } i18n.apply(document); });
406 window.chjPanels = { active, syncSessions, transfersFinished, view };
407 void (async () => {
408 try { view.places = await local.places(); } catch {}
409 for (let index = 0; index < 3; index += 1) { const panel = createPanel(index); view.panels.push(panel); $("panelGrid").append(panel.root); }
410 view.panels[0].touched = true;
411 setSource(view.panels[0], LOCAL);
412 for (const panel of view.panels) renderSources(panel);
413 i18n.apply(document);
414 applyMode();
415 })();
416})();
SHA-256696ff66ec3ce4b2d49bf612ce659f40cf44fd09a6eb1bcb9102f99e83aa47075

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df
Soubory