New __imap*/__smtpsend/__eval Jet tags give templates real IMAP (list/message/flag/delete/move/search/folders/accounts) and SMTP-send capability, backing the new /email webmail client (email.jet/ email.menu, assets/images/email.js + newemail.js) and its mainpagetemplate.jet-based page shell. Most of this tag family's source was already being built into the live site (Tag.cpp already registered __imap/__imaplist/__imapmessage/__imapflag/__imapdelete/ __eval) but had never actually been committed; this also registers and commits the remaining five (__imapmove/__imapfolders/ __imapsearch/__smtpsend/__imapaccounts). admin.page/admin.menu gain a real subpage tier (admin-email, admin-newemail, admin-invoices, admin-products, admin-users) replacing several previously top-level pages, mirrored by a similar opensource.page subpage tier (opensource-overview/asm1802/barantmail/ jet/serverframework/synthmax) - old top-level pages retired via git rename (asm1802.page, jet.page, serverframework.page, admin_invoices.page); barantmail.page is dropped outright, folded into opensource-barantmail.subpage. New storefront (store.page, store-createstore.subpage, barantstore.page) and cart flow (cart.jet, updatecartqty.proc, removecartitem.proc, editproduct.page/selectproduct.proc/ update_product.proc for inventory maintenance against the barant database). New document upload/download manager (documents.jet, documents_api.jet, documents_upload.proc, documents_delete.proc, assets/images/documents.js + documents-page.js) with a persistent floating panel, and a rich-text page editor (htmleditor.jet/.page, createhtmleditor.proc, savehtmleditor.proc, assets/images/htmleditor.js + its icon/logo assets). SESSIONCTL.tag: login now redirects back to the originally-requested page instead of always landing on /account ($[start:0] vs $[start] variable-name mismatch). main.css/menu.bar/invoices.menu/jet_overview.section/login.menu: styling and navigation updates for the above; new site assets (fonts, images, audio) referenced by them. jetc/build-assets.sh, list_jet_files.sh, lowercase_jet.py, strip_fragments.py: small dev tooling used while building the above.
179 lines
6.3 KiB
JavaScript
179 lines
6.3 KiB
JavaScript
// Site-wide file transfer manager - loaded once via mainpagetemplate.jet
|
|
// (see its own script tag, outside #content) so the floating panel and
|
|
// any in-progress transfer survive spa.js's own #content-only swaps
|
|
// when navigating to a different page mid-upload/download.
|
|
(function() {
|
|
var panel = document.getElementById('filemgr-panel');
|
|
if (!panel)
|
|
return;
|
|
|
|
var toggle = document.getElementById('filemgr-toggle');
|
|
var badge = document.getElementById('filemgr-badge');
|
|
var list = document.getElementById('filemgr-list');
|
|
var clearBtn = document.getElementById('filemgr-clear');
|
|
|
|
var transfers = {};
|
|
var nextId = 1;
|
|
|
|
function escapeHtml(s) {
|
|
var div = document.createElement('div');
|
|
div.textContent = s == null ? '' : String(s);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function updateBadge() {
|
|
var activeCount = 0;
|
|
for (var k in transfers) {
|
|
if (transfers[k].status === 'active')
|
|
activeCount++;
|
|
}
|
|
badge.textContent = activeCount > 0 ? String(activeCount) : '';
|
|
}
|
|
|
|
function removeEmptyPlaceholder() {
|
|
var empty = document.getElementById('filemgr-empty');
|
|
if (empty)
|
|
empty.remove();
|
|
}
|
|
|
|
function showEmptyPlaceholderIfNeeded() {
|
|
if (Object.keys(transfers).length === 0 && !document.getElementById('filemgr-empty'))
|
|
list.innerHTML = '<div id="filemgr-empty">No transfers yet.</div>';
|
|
}
|
|
|
|
var UPLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V6M5 12l7-7 7 7"/></svg>';
|
|
var DOWNLOAD_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v13M5 12l7 7 7-7"/></svg>';
|
|
|
|
function addTransfer(name, direction) {
|
|
var id = 'xfer' + (nextId++);
|
|
removeEmptyPlaceholder();
|
|
|
|
var el = document.createElement('div');
|
|
el.className = 'filemgr-item';
|
|
el.innerHTML =
|
|
'<div class="filemgr-item-row">' +
|
|
'<span class="filemgr-item-icon">' + (direction === 'upload' ? UPLOAD_ICON : DOWNLOAD_ICON) + '</span>' +
|
|
'<span class="filemgr-item-name">' + escapeHtml(name) + '</span>' +
|
|
'<span class="filemgr-item-status">0%</span>' +
|
|
'</div>' +
|
|
'<div class="filemgr-progress-track"><div class="filemgr-progress-fill"></div></div>';
|
|
list.insertBefore(el, list.firstChild);
|
|
|
|
transfers[id] = { el: el, status: 'active' };
|
|
panel.classList.add('filemgr-expanded');
|
|
updateBadge();
|
|
return id;
|
|
}
|
|
|
|
function updateProgress(id, percent) {
|
|
var t = transfers[id];
|
|
if (!t)
|
|
return;
|
|
t.el.querySelector('.filemgr-progress-fill').style.width = percent + '%';
|
|
t.el.querySelector('.filemgr-item-status').textContent = percent + '%';
|
|
}
|
|
|
|
function markDone(id, statusText) {
|
|
var t = transfers[id];
|
|
if (!t)
|
|
return;
|
|
t.status = 'done';
|
|
t.el.classList.add('filemgr-done');
|
|
t.el.querySelector('.filemgr-progress-fill').style.width = '100%';
|
|
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Done';
|
|
updateBadge();
|
|
}
|
|
|
|
function markFailed(id, statusText) {
|
|
var t = transfers[id];
|
|
if (!t)
|
|
return;
|
|
t.status = 'failed';
|
|
t.el.classList.add('filemgr-failed');
|
|
t.el.querySelector('.filemgr-item-status').textContent = statusText || 'Failed';
|
|
updateBadge();
|
|
}
|
|
|
|
// Uploads a FileList/array of File objects, each as its own
|
|
// concurrent XHR request - the browser's own HTTP connection pooling
|
|
// runs several at once, tracked here as separate panel entries with
|
|
// independent progress via xhr.upload's own progress event (fetch()
|
|
// has no equivalent for the request/upload side, only the response).
|
|
window.fileManagerUpload = function(files) {
|
|
for (var i = 0; i < files.length; i++) {
|
|
(function(file) {
|
|
var id = addTransfer(file.name, 'upload');
|
|
var xhr = new XMLHttpRequest();
|
|
transfers[id].xhr = xhr;
|
|
xhr.open('POST', '/documents/api/upload');
|
|
xhr.upload.addEventListener('progress', function(e) {
|
|
if (e.lengthComputable)
|
|
updateProgress(id, Math.round((e.loaded / e.total) * 100));
|
|
});
|
|
xhr.addEventListener('load', function() {
|
|
var res = null;
|
|
try { res = JSON.parse(xhr.responseText); } catch (e) {}
|
|
if (xhr.status === 200 && res && res.ok) {
|
|
markDone(id);
|
|
document.dispatchEvent(new CustomEvent('documents:changed'));
|
|
} else {
|
|
markFailed(id, (res && res.error) || 'Failed');
|
|
}
|
|
});
|
|
xhr.addEventListener('error', function() { markFailed(id); });
|
|
var fd = new FormData();
|
|
fd.append('name', file.name);
|
|
fd.append('filedata', file);
|
|
xhr.send(fd);
|
|
})(files[i]);
|
|
}
|
|
};
|
|
|
|
// Downloads a document by id via XHR (responseType 'blob') instead
|
|
// of a plain <a href> so it gets real tracked progress in the same
|
|
// panel as uploads, then hands the fetched bytes to the browser's
|
|
// normal save flow via a Blob URL + synthetic <a download> click.
|
|
window.fileManagerDownload = function(id, name) {
|
|
var xferId = addTransfer(name, 'download');
|
|
var xhr = new XMLHttpRequest();
|
|
transfers[xferId].xhr = xhr;
|
|
xhr.open('GET', '/documents/download?id=' + encodeURIComponent(id));
|
|
xhr.responseType = 'blob';
|
|
xhr.addEventListener('progress', function(e) {
|
|
if (e.lengthComputable)
|
|
updateProgress(xferId, Math.round((e.loaded / e.total) * 100));
|
|
});
|
|
xhr.addEventListener('load', function() {
|
|
if (xhr.status !== 200) {
|
|
markFailed(xferId);
|
|
return;
|
|
}
|
|
var blobUrl = URL.createObjectURL(xhr.response);
|
|
var a = document.createElement('a');
|
|
a.href = blobUrl;
|
|
a.download = name;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 30000);
|
|
markDone(xferId);
|
|
});
|
|
xhr.addEventListener('error', function() { markFailed(xferId); });
|
|
xhr.send();
|
|
};
|
|
|
|
toggle.addEventListener('click', function() {
|
|
panel.classList.toggle('filemgr-expanded');
|
|
});
|
|
|
|
clearBtn.addEventListener('click', function() {
|
|
for (var k in transfers) {
|
|
if (transfers[k].status !== 'active') {
|
|
transfers[k].el.remove();
|
|
delete transfers[k];
|
|
}
|
|
}
|
|
showEmptyPlaceholderIfNeeded();
|
|
});
|
|
})();
|