Seregon/zftpd

Zero-copy FTP/HTTP Daemon compatible with all POSIX systems

C/11.0 KB/No license
web/js/views/file-manager.js
zftpd / web / js / views / file-manager.js
1/* ══ FILE MANAGER VIEW — FileZilla-style Dual Pane ════════════════════════
2 * Split view with source and destination panels.
3 * ES5 compatible for PS5 browser.
4 * ═════════════════════════════════════════════════════════════════════════ */
5 
6var ZFTPD = ZFTPD || {};
7 
8(function (Z) {
9 'use strict';
10 
11 var D = document;
12 var $ = Z.$;
13 var ICO = Z.ICO;
14 
15 var fm = {};
16 var _leftPath = '/';
17 var _rightPath = '/';
18 var _leftEntries = [];
19 var _rightEntries = [];
20 var _leftSelected = [];
21 var _rightSelected = [];
22 var _activePanel = 'left';
23 
24 fm.init = function () {
25 _leftPath = Z.state.fmLeftPath || '/';
26 _rightPath = Z.state.fmRightPath || '/';
27 loadPanel('left', _leftPath);
28 loadPanel('right', _rightPath);
29 wireButtons();
30 };
31 
32 /* ── Load a panel ── */
33 function loadPanel(side, path) {
34 path = Z.norm(path);
35 if (side === 'left') { _leftPath = path; Z.state.fmLeftPath = path; }
36 else { _rightPath = path; Z.state.fmRightPath = path; }
37 
38 var body = $('fm-' + side + '-body');
39 if (body) body.innerHTML = '<div style="padding:20px;text-align:center;"><div class="loader" style="margin:0 auto;"></div></div>';
40 
41 renderPanelPath(side, path);
42 
43 Z.api.list(path).then(function (d) {
44 var entries = (d && Array.isArray(d.entries)) ? d.entries : [];
45 /* Sort: dirs first, then alpha */
46 entries.sort(function (a, b) {
47 var da = a.type === 'directory' ? 0 : 1;
48 var db = b.type === 'directory' ? 0 : 1;
49 if (da !== db) return da - db;
50 return (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase());
51 });
52 
53 if (side === 'left') { _leftEntries = entries; _leftSelected = []; }
54 else { _rightEntries = entries; _rightSelected = []; }
55 
56 renderPanel(side, entries);
57 updateFooter(side, entries);
58 }).catch(function () {
59 if (body) body.innerHTML = '<div style="padding:20px;text-align:center;color:var(--er);font-size:12px;">Failed to load</div>';
60 });
61 }
62 
63 /* ── Render panel breadcrumb ── */
64 function renderPanelPath(side, path) {
65 var bc = $('fm-' + side + '-path');
66 if (!bc) return;
67 bc.innerHTML = '';
68 
69 var root = D.createElement('span');
70 root.className = 'crumb' + (path === '/' ? ' act' : '');
71 root.textContent = '/';
72 root.onclick = function () { loadPanel(side, '/'); };
73 bc.appendChild(root);
74 
75 var parts = Z.norm(path).split('/');
76 var acc = '';
77 for (var i = 0; i < parts.length; i++) {
78 if (!parts[i]) continue;
79 acc += '/' + parts[i];
80 var sep = D.createElement('span');
81 sep.className = 'cr-sep';
82 sep.textContent = '/';
83 bc.appendChild(sep);
84 var seg = D.createElement('span');
85 seg.className = 'crumb' + (acc === path ? ' act' : '');
86 seg.textContent = parts[i];
87 (function (cp) { seg.onclick = function () { loadPanel(side, cp); }; })(acc);
88 bc.appendChild(seg);
89 }
90 }
91 
92 /* ── Render panel file rows ── */
93 function renderPanel(side, entries) {
94 var body = $('fm-' + side + '-body');
95 if (!body) return;
96 body.innerHTML = '';
97 
98 var path = side === 'left' ? _leftPath : _rightPath;
99 
100 /* Parent directory row */
101 if (path !== '/') {
102 var parentRow = D.createElement('div');
103 parentRow.className = 'fm-row';
104 parentRow.innerHTML =
105 '<div class="fm-row-ico fi-dir">' + ICO.arrowUp + '</div>' +
106 '<div class="fm-row-name">..</div>' +
107 '<div class="fm-row-size"></div>';
108 parentRow.ondblclick = function () {
109 loadPanel(side, Z.parent(path) || '/');
110 };
111 body.appendChild(parentRow);
112 }
113 
114 if (!entries.length) {
115 body.innerHTML += '<div style="padding:16px;text-align:center;color:var(--tx3);font-size:11px;">Empty</div>';
116 return;
117 }
118 
119 for (var i = 0; i < entries.length; i++) {
120 var e = entries[i];
121 var isDir = e.type === 'directory';
122 var fullPath = Z.join(path, e.name);
123 
124 var row = D.createElement('div');
125 row.className = 'fm-row';
126 row.setAttribute('data-index', i);
127 row.setAttribute('data-path', fullPath);
128 row.setAttribute('data-name', e.name);
129 row.setAttribute('data-dir', isDir ? '1' : '0');
130 
131 var cat = Z.fileCategory(e.name, isDir);
132 row.innerHTML =
133 '<div class="fm-row-ico fi-' + cat + '">' + (isDir ? ICO.folder : ICO.file) + '</div>' +
134 '<div class="fm-row-name" title="' + e.name + '">' + e.name + '</div>' +
135 '<div class="fm-row-size">' + (isDir ? '' : Z.bytes(e.size || 0)) + '</div>';
136 
137 /* Click to select */
138 (function (r, idx, s) {
139 r.onclick = function (ev) {
140 _activePanel = s;
141 var sel = s === 'left' ? _leftSelected : _rightSelected;
142 if (ev.ctrlKey || ev.metaKey) {
143 var pos = sel.indexOf(idx);
144 if (pos >= 0) sel.splice(pos, 1);
145 else sel.push(idx);
146 } else {
147 if (s === 'left') _leftSelected = [idx];
148 else _rightSelected = [idx];
149 sel = s === 'left' ? _leftSelected : _rightSelected;
150 }
151 updateSelection(s);
152 };
153 r.ondblclick = function () {
154 var isD = r.getAttribute('data-dir') === '1';
155 if (isD) loadPanel(s, r.getAttribute('data-path'));
156 else window.location.href = Z.api.downloadUrl(r.getAttribute('data-path'));
157 };
158 })(row, i, side);
159 
160 body.appendChild(row);
161 }
162 }
163 
164 /* ── Update selection highlighting ── */
165 function updateSelection(side) {
166 var body = $('fm-' + side + '-body');
167 if (!body) return;
168 var sel = side === 'left' ? _leftSelected : _rightSelected;
169 var rows = body.querySelectorAll('.fm-row[data-index]');
170 for (var i = 0; i < rows.length; i++) {
171 var idx = parseInt(rows[i].getAttribute('data-index'), 10);
172 rows[i].classList.toggle('selected', sel.indexOf(idx) >= 0);
173 }
174 }
175 
176 /* ── Update footer stats ── */
177 function updateFooter(side, entries) {
178 var footer = $('fm-' + side + '-footer');
179 if (!footer) return;
180 var dirs = 0, files = 0, totalSize = 0;
181 for (var i = 0; i < entries.length; i++) {
182 if (entries[i].type === 'directory') dirs++;
183 else { files++; totalSize += entries[i].size || 0; }
184 }
185 footer.innerHTML = '<b>' + dirs + '</b> folders, <b>' + files + '</b> files \u2014 ' + Z.bytes(totalSize);
186 }
187 
188 /* ── Wire center action buttons ── */
189 function wireButtons() {
190 var copyRight = $('fm-copy-right');
191 if (copyRight) copyRight.onclick = function () { doCopy('left', 'right'); };
192 var copyLeft = $('fm-copy-left');
193 if (copyLeft) copyLeft.onclick = function () { doCopy('right', 'left'); };
194 var delBtn = $('fm-delete');
195 if (delBtn) delBtn.onclick = function () { doDeleteSelected(); };
196 var refreshLeft = $('fm-refresh-left');
197 if (refreshLeft) refreshLeft.onclick = function () { loadPanel('left', _leftPath); };
198 var refreshRight = $('fm-refresh-right');
199 if (refreshRight) refreshRight.onclick = function () { loadPanel('right', _rightPath); };
200 }
201 
202 /* ── Copy selected files from one panel to the other ── */
203 function doCopy(fromSide, toSide) {
204 if (!Z.ensureTransferIdle()) return;
205 var sel = fromSide === 'left' ? _leftSelected : _rightSelected;
206 var entries = fromSide === 'left' ? _leftEntries : _rightEntries;
207 var fromPath = fromSide === 'left' ? _leftPath : _rightPath;
208 var toPath = toSide === 'left' ? _leftPath : _rightPath;
209 
210 if (!sel.length) {
211 Z.toast('Select files first', 'wn');
212 return;
213 }
214 
215 var i = 0;
216 var cancelled = false;
217 
218 function next() {
219 if (cancelled || i >= sel.length) {
220 Z.hideTransferLock();
221 loadPanel(toSide, toPath);
222 if (!cancelled && i > 0) {
223 Z.notify('Copy complete', i + ' items copied to ' + toPath, 'ok');
224 /* Clear selection */
225 if (fromSide === 'left') _leftSelected = []; else _rightSelected = [];
226 loadPanel(fromSide, fromPath);
227 }
228 return;
229 }
230 var entry = entries[sel[i++]];
231 if (!entry) { next(); return; }
232 
233 var srcPath = Z.join(fromPath, entry.name);
234 
235 Z.showTransferLock({
236 label: 'COPYING',
237 filename: entry.name,
238 dest: toPath,
239 onCancel: function () {
240 cancelled = true;
241 Z.api.copyCancel().catch(function(){}); // Assuming there's a cancel endpoint
242 Z.hideTransferLock();
243 Z.notify('Copy cancelled', entry.name, 'wn');
244 }
245 });
246 
247 /* Fake progress based on time for now, or use an API polling if available */
248 var startTime = Date.now();
249 var fakeProgressInterval = setInterval(function() {
250 var elapsed = Math.floor((Date.now() - startTime) / 1000);
251 // Z.api.copyProgress() would go here in a real implementation
252 Z.updateTransferLock({ elapsed: elapsed + 's' });
253 }, 1000);
254 
255 Z.api.copy(srcPath, toPath, entry.size || 0).then(function () {
256 clearInterval(fakeProgressInterval);
257 next();
258 }).catch(function (e) {
259 clearInterval(fakeProgressInterval);
260 Z.hideTransferLock();
261 Z.notify('Copy failed', entry.name + ': ' + e.message, 'er');
262 });
263 }
264 next();
265 }
266 
267 /* ── Delete selected files in active panel ── */
268 function doDeleteSelected() {
269 var sel = _activePanel === 'left' ? _leftSelected : _rightSelected;
270 var entries = _activePanel === 'left' ? _leftEntries : _rightEntries;
271 var path = _activePanel === 'left' ? _leftPath : _rightPath;
272 
273 if (!sel.length) {
274 Z.toast('Select files first', 'wn');
275 return;
276 }
277 
278 var names = sel.map(function (idx) { return entries[idx] ? entries[idx].name : ''; }).join(', ');
279 Z.modal.confirm('Delete ' + sel.length + ' item(s)', names, true).then(function (ok) {
280 if (!ok) return;
281 var i = 0;
282 function next() {
283 if (i >= sel.length) {
284 loadPanel(_activePanel, path);
285 Z.toast('Deleted', 'ok');
286 return;
287 }
288 var entry = entries[sel[i++]];
289 if (!entry) { next(); return; }
290 var fullPath = Z.join(path, entry.name);
291 Z.api.del(fullPath, entry.type === 'directory').then(function () {
292 next();
293 }).catch(function (e) {
294 Z.toast('Delete failed: ' + e.message, 'er');
295 });
296 }
297 next();
298 });
299 }
300 
301 Z.fileManager = fm;
302 
303})(ZFTPD);
304