afi = { consts: {}, params: {}, elements: {}, utils: {}, rpc: {}, uploader: {}, interface: {}, directcall: {}, } afi.consts = { leftPageWarn: "确认放弃正在进行的传输?", ensureDrop: () => !confirm('确认移动此对象?') } afi.params = { readonly: readonly, extraPath: extraPath } afi.elements = { itemlinks: Array.from(document.querySelectorAll('a.item-links')), table: document.getElementById("index-table"), badge: document.getElementById("badge"), uploader: document.getElementById("clickupload"), path: document.getElementById("path"), nav: document.getElementById("nav"), title: document.head.querySelector("title"), dropGrid: document.getElementById("drop-grid"), barDisplay: document.getElementById("bar-display"), barProc: document.getElementById("bar-proc"), helpPanel: document.getElementById('help-panel'), helpToggle: document.getElementById('help-toggle'), } afi.pulsar = { pulseLeft: 0, pulseNotificator(text) { afi.elements.badge.innerText = text this.pulseEffect(afi.elements.badge) }, pulseSuccess() { this.pulseNotificator("操作成功") }, pulseFailure(error) { this.pulseNotificator("操作失败 " + error) }, pulseEffect: function (element) { element.style.display = "block" afi.pulsar.pulseLeft += 1 element.classList.remove('pulse'); void element.offsetWidth; element.classList.add('pulse'); setTimeout(function () { afi.pulsar.pulseLeft -= 1; if (afi.pulsar.pulseLeft > 0) { console.log("[AFI] Notification stack", afi.pulsar.pulseLeft, "left, skip clear") } else { console.log("[AFI] Notification stack", afi.pulsar.pulseLeft, "left, do clear") element.style.display = 'none'; } }, 5000); }, } afi.utils = { isDupe: function (itemname) { if (afi.elements.itemlinks.find(a => a.innerText.replace('/', '') === itemname)) { alert('名称 ' + itemname + ' 已被已存在的项目占用'); return true } else { return false } }, isFolder: function (element) { return element && element.href && element.innerText.endsWith('/'); }, encodeURIHash: function (e) { return encodeURI(e).replaceAll('#', '%23'); }, refresh: function () { afi.interface.browseTo(location.href, true); }, prependPath: function (a) { return a.startsWith('/') ? a : decodeURI(location.pathname) + a; }, cancelDefault(e) { e.preventDefault() e.stopPropagation() }, setHighlight: function (t) { t.classList.add('highlight') }, getHighlight: function () { return document.querySelector('.highlight') || {} }, resetHighlight: function () { try { this.getHighlight().classList.remove('highlight') } catch (e) { /* */ } }, isHelpMode: () => afi.elements.helpPanel.style.display === 'block', helpOn: function () { afi.elements.helpPanel.style.display = 'block' afi.elements.table.style.display = 'none' afi.elements.helpToggle.innerText = "关闭帮助" afi.elements.helpToggle.onclick = afi.utils.helpOff }, helpOff: function () { if (!afi.utils.isHelpMode()) return afi.elements.helpPanel.style.display = 'none' afi.elements.table.style.display = 'table' afi.elements.helpToggle.innerText = "帮助菜单" afi.elements.helpToggle.onclick = afi.utils.helpOn } } afi.rpc = { rpc: function (call, args, cb) { console.log('[AFI] RPC', call, args) const xhr = new XMLHttpRequest() xhr.open('POST', location.origin + afi.params.extraPath + '/rpc') xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8') xhr.send(JSON.stringify({ call, args })) xhr.onload = cb xhr.onerror = () => afi.pulsar.pulseFailure() }, mkdirCall: function (path, cb) { this.rpc('mkdirp', [afi.utils.prependPath(path)], cb); }, rmCall: function (path1, cb) { this.rpc('rm', [afi.utils.prependPath(path1)], cb); }, mvCall: function (path1, path2, cb) { this.rpc('mv', [path1, path2], cb); }, sumCall: function (path, type, cb) { this.rpc('sum', [afi.utils.prependPath(path), type], cb); }, touchCall: function (path, cb) { this.rpc('touch', [afi.utils.prependPath(path)], cb); } } afi.uploader = { totalUploads: 0, totalUploaded: 0, totalUploadsSize: 0, totalUploadedSize: [], shouldRefresh: function () { this.totalUploaded += 1 console.log('[AFI] Have uploaded ' + this.totalUploaded + ' files') if (this.totalUploads === this.totalUploaded) { window.onbeforeunload = null console.log('[AFI] Finished after uploaded ' + this.totalUploaded + ' files') this.totalUploaded = 0 this.totalUploads = 0 this.totalUploadsSize = 0 this.totalUploadedSize = [] setTimeout(afi.utils.refresh, 200) afi.elements.barDisplay.style.display = 'none' //afi.elements.barProc.innerText = "传输: 就绪" } }, updatePercent: function (event) { this.totalUploadedSize[event.target.id] = event.loaded const ttlDone = this.totalUploadedSize.reduce((s, x) => s + x) const percent = Math.min(Math.floor(100 * ttlDone / this.totalUploadsSize), 100) + '%' // 此处消除对小文件元数据上传带来的误差, 理论上是不准的 afi.elements.barProc.innerText = "传输: " + percent }, upload: function (id, what, path, cbDone, cbErr, cbUpdate) { const xhr = new XMLHttpRequest() xhr.open('POST', location.origin + afi.params.extraPath + '/post') xhr.setRequestHeader('afi-path', path) xhr.upload.addEventListener('load', cbDone) xhr.upload.addEventListener('progress', cbUpdate) xhr.upload.addEventListener('error', cbErr) xhr.upload.id = id xhr.send(what) }, uploadFile: function (file, path) { if (afi.params.readonly) return path = decodeURI(location.pathname).slice(0, -1) + path window.onbeforeunload = afi.consts.leftPageWarn afi.elements.barProc.style.display = afi.elements.barDisplay.style.display = 'block' this.totalUploads += 1 this.totalUploadsSize += file.size //this.totalUploadedSize[this.totalUploads] = file.size if (typeof upBarName !== 'undefined') { upBarName.innerText = this.totalUploads > 1 ? this.totalUploads + ' 个文件' : file.name } const formData = new FormData() formData.append(file.name, file) this.upload(this.totalUploads, formData, encodeURIComponent(path), this.shouldRefresh.bind(this), null, this.updatePercent.bind(this)) } } afi.interface = { pushEntry: function (entry) { if (!entry.webkitGetAsEntry && !entry.getAsEntry) { return alert('不兼容此浏览器') } else { entry = entry.webkitGetAsEntry() || entry.getAsEntry() } this.parseDomItem.bind(this)(entry, true) }, parseDomFolder: function (f) { f.createReader().readEntries(e => e.forEach(i => afi.interface.parseDomItem.bind(this)(i))) }, parseDomItem: function (domFile, shouldCheckDupes) { if (shouldCheckDupes && afi.utils.isDupe(domFile.name)) { return } if (domFile.isFile) { domFile.file(f => afi.uploader.uploadFile(f, domFile.fullPath)) } else { const f = domFile.fullPath.startsWith('/') ? domFile.fullPath.slice(1) : domFile.fullPath afi.rpc.mkdirCall(f, () => this.parseDomFolder.bind(this)(domFile)) } }, browseTo: async function (href, pulseEffectDone, skipHistory) { try { const r = await fetch(href, { credentials: 'include' }) const t = await r.text() const parsed = new DOMParser().parseFromString(t, 'text/html') afi.elements.table.innerHTML = parsed.getElementById('index-table').innerHTML const nav = parsed.getElementById('nav').innerText const path = parsed.getElementById('path').innerText const title = parsed.head.querySelector('title').innerText if (afi.elements.path.innerText !== path) { if (!skipHistory) { const escaped = afi.utils.encodeURIHash(afi.params.extraPath + path) history.pushState({}, '', escaped) } afi.elements.title.innerText = title afi.elements.nav.innerText = nav afi.elements.path.innerText = path this.setBreadcrumbs() } init() if (pulseEffectDone) afi.pulsar.pulseSuccess() } catch (error) { afi.pulsar.pulseFailure(error) } }, setBreadcrumbs: function () { const parent = afi.elements.nav.parentNode; afi.elements.nav.outerHTML = '' + afi.elements.path.innerText.split('/').join('/') + '' afi.elements.nav = parent.querySelector('#nav'); } } afi.directcall = { exec_mkdir: function () { const folder = prompt('新建目录名称', '') if (folder && !afi.utils.isDupe(folder)) { afi.rpc.mkdirCall(folder, afi.utils.refresh) } }, exec_touch: function () { const file = prompt('新建文件名称', '') if (file && !afi.utils.isDupe(file)) { afi.rpc.touchCall(file, afi.utils.refresh) } }, exec_rm: function () { const file = prompt('删除对象名称', '') afi.rpc.rmCall(file, afi.utils.refresh) }, exec_mv: function () { const orig = prompt('源地址', '') const dest = prompt('目标地址', '') if (orig && !afi.utils.isDupe(dest)) { afi.rpc.mvCall(afi.utils.prependPath(orig), afi.utils.prependPath(dest), afi.utils.refresh) } }, bread_click: function (e) { const p = Array.from(document.getElementById('nav').childNodes).map(k => k.innerText) const i = p.findIndex(s => s === e.target.innerText) var dst = p.slice(0, i + 1).join('').slice(1) if (!dst.startsWith('/')) { dst = "/" + dst } const target = location.origin + afi.params.extraPath + afi.utils.encodeURIHash(dst) afi.interface.browseTo(target, false) } } afi.checksum = { getSum(type) { upBarPc.style.display = 'block' upBarPc.innerText = '远程求解校验和...' upBarPc.style.width = '100%' sumsOff() sumCall(getASelected().innerText, type, loaded => { navigator.clipboard.writeText(loaded.target.responseText) // 复制到剪贴板 upBarPc.style.display = 'none' flicker(okBadge) }) }, isSumsMode: () => sums.style.display === 'block', sumsToggle: () => isSumsMode() ? sumsOff() : sumsOn(), sumsOn() { if (isFolder(getASelected())) { alert('无法对目录求解校验和') return } sums.style.display = 'block' table.style.display = 'none' }, sumsOff() { if (!isSumsMode()) return sums.style.display = 'none' table.style.display = 'table' return true } } function init() { afi.elements = { itemlinks: Array.from(document.querySelectorAll('a.item-links')), table: document.getElementById("index-table"), badge: document.getElementById("badge"), path: document.getElementById("path"), uploader: document.getElementById("clickupload"), nav: document.getElementById("nav"), title: document.head.querySelector("title"), dropGrid: document.getElementById("drop-grid"), barDisplay: document.getElementById("bar-display"), barProc: document.getElementById("bar-proc"), helpPanel: document.getElementById('help-panel'), helpToggle: document.getElementById('help-toggle'), } afi.interface.setBreadcrumbs() afi.elements.uploader.addEventListener('change', () => { const files = Array.from(afi.elements.uploader.files); files.forEach(file => { if (!afi.utils.isDupe(file.name)) { afi.uploader.uploadFile(file, '/' + file.name); } }); }, false); afi.elements.dropGrid.ondragend = afi.elements.dropGrid.ondragexit = afi.elements.dropGrid.ondragleave = e => { afi.utils.cancelDefault(e) afi.elements.dropGrid.style.display = 'none' } let draggingSrc document.ondragenter = e => { if (afi.params.readonly) { return } afi.utils.cancelDefault(e) afi.utils.resetHighlight() if (!draggingSrc) { afi.elements.dropGrid.style.display = 'flex' e.dataTransfer.dropEffect = 'copy' } else if (draggingSrc) { const t = getClosestRow(e.target) afi.utils.isFolder(t.firstChild) && setHighlight(t) } } afi.elements.itemlinks.forEach(link => { const row = link.parentElement.parentElement.parentElement.querySelector('.item-icon'); if (!row) return; let clickTimer = null; let isDoubleClick = false; row.addEventListener('click', (e) => { if (afi.params.readonly || link.innerText === '../') return; if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } clickTimer = setTimeout(() => { if (!isDoubleClick) { e.stopPropagation(); if (confirm('确认删除此项目?')) { afi.rpc.rmCall(afi.utils.prependPath(link.href), afi.utils.refresh); } } isDoubleClick = false; clickTimer = null; }, 300); }); row.addEventListener('dblclick', (e) => { e.stopPropagation(); isDoubleClick = true; if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } if (afi.params.readonly) return; if (link.innerText === '../') return; // 不能操作上级 const newName = prompt('新名称或目标地址', link.innerText); if (newName && !afi.utils.isDupe(newName)) { afi.rpc.mvCall( afi.utils.prependPath(link.innerText), afi.utils.prependPath(newName), afi.utils.refresh ); } setTimeout(() => { isDoubleClick = false; }, 100); }); }); document.ondragstart = e => { draggingSrc = e.target.innerHTML } // 记录拖拽源 document.ondragend = e => resetHighlight() // 清理高亮 document.ondragover = e => { afi.utils.cancelDefault(e); return false } // 允许拖拽 document.ondrop = e => { if (afi.params.readonly) return afi.utils.cancelDefault(e) afi.elements.dropGrid.style.display = 'none' const t = afi.utils.getHighlight().firstChild if (draggingSrc && t) { const dest = t.innerHTML + draggingSrc afi.consts.ensureDrop() || afi.rpc.mvCall(afi.utils.prependPath(draggingSrc), afi.utils.prependPath(dest), afi.utils.refresh) } else if (e.dataTransfer.items.length) { Array.from(e.dataTransfer.items).forEach(afi.interface.pushEntry.bind(afi.interface)) } afi.utils.resetHighlight() draggingSrc = null return false } if (!afi.params.readonly) { afi.elements.barProc.innerText = "传输: 就绪" } console.log('[AFI] Initialized') } window.onload = init