This commit is contained in:
2026-04-02 07:45:43 +08:00
parent c4b7fdd67e
commit 4a55a4bc36
8 changed files with 1197 additions and 51 deletions

4
ui/DESCRIPT.ION Normal file
View File

@@ -0,0 +1,4 @@
favicon.svg 图标文件
script.js 前端脚本
style.css 样式表
ui.html 前端模板

1
ui/favicon.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1774691828293" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="19252" data-spm-anchor-id="a313x.collections_detail.0.i3.40373a81qeY5sg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M811.4 418.7C765.6 297.9 648.9 212 512.2 212S258.8 297.8 213 418.6C127.3 441.1 64 519.1 64 612c0 110.5 89.5 200 199.9 200h496.2C870.5 812 960 722.5 960 612c0-92.7-63.1-170.7-148.6-193.3z m36.3 281c-23.4 23.4-54.5 36.3-87.6 36.3H263.9c-33.1 0-64.2-12.9-87.6-36.3-23.4-23.4-36.3-54.6-36.3-87.7 0-28 9.1-54.3 26.2-76.3 16.7-21.3 40.2-36.8 66.1-43.7l37.9-9.9 13.9-36.6c8.6-22.8 20.6-44.1 35.7-63.4 14.9-19.2 32.6-35.9 52.4-49.9 41.1-28.9 89.5-44.2 140-44.2s98.9 15.3 140 44.2c19.9 14 37.5 30.8 52.4 49.9 15.1 19.3 27.1 40.7 35.7 63.4l13.8 36.5 37.8 10c54.3 14.5 92.1 63.8 92.1 120 0 33.1-12.9 64.3-36.3 87.7z" p-id="19253" fill="#1659a3"></path></svg>

After

Width:  |  Height:  |  Size: 1023 B

469
ui/script.js Normal file
View File

@@ -0,0 +1,469 @@
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 = '<span id="nav" onclick="return afi.directcall.bread_click(event)"><span class="nav">' + afi.elements.path.innerText.split('/').join('/</span><span class="nav">') + '</span></span>'
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

348
ui/style.css Normal file
View File

@@ -0,0 +1,348 @@
@import url('https://fonts.loli.net/css2?family=Cascadia+Code:ital,wght@0,200..700;1,200..700&display=swap');
@import url("https://fonts.loli.net/css2?family=Noto+Sans+SC:wght@400;700&display=swap");
:root {
--background: #ffffff;
--background-darker: #eeeeee;
--background-verydarker: #dddddd;
--foreground: #454545;
--accent: #0f59a4;
--accent-lighten: #2793ff;
/*本来有color-mix的, 可惜支持太新*/
--radius: 0;
--font-size: 1rem;
--line-height: 1.54em;
}
html {
/* 预留滚动条空间,但不会让背景色断开,保持内容稳定 */
scrollbar-gutter: stable;
}
*,
*::before,
*::after {
background-color: var(--background);
color: var(--foreground);
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: flex;
flex-direction: column;
font-family: "Cascadia Code", "Noto Sans SC", system-ui, sans-serif;
background: var(--background);
}
header {
position: sticky;
top: 0;
display: flex;
justify-content: space-between;
align-items: center;
background: var(--background);
padding: 4px 32px 4px 20px;
border-bottom: 1px solid var(--background-verydarker);
z-index: 100;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.logo-img {
display: inline-block;
height: calc(8px + 1em);
width: auto;
padding: 0 1em 0 0;
vertical-align: middle;
}
#nav {
flex: 1;
font-size: 14px;
color: var(--foreground);
}
#operations {
display: flex;
gap: 8px;
}
.operation {
padding: 4px 0px;
border-color: transparent;
color: var(--accent);
cursor: pointer;
font-size: 14px;
}
.ellipsis {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
}
#index-table {
margin: 20px 20px 0 20px;
width: auto;
border-collapse: collapse;
background: var(--background);
overflow: auto;
font-size: 12px;
}
#index-table th,
#index-table td {
/*hack td高度计算的怪异标准(明确指定), 不然icon的div没高度显示不了icon*/
height: 1px;
text-align: left;
/*Noto Fonts SC 和 Cascadia Code(以及各种utf-8 fallback字体) 的em不同, 所以用父元素统一基准, 不用子元素(可能没中文), 不然icon大小不齐*/
/*然后导致rem不能有可变的多语言内容, 不然页面之间照样变*/
/*css设计时就不能设置以指定字体为参照的em吗? 本来这个问题能用ie盒模型+"指定字体e"解决的 这下非要导致每行content间距可能不一样*/
padding: 0.25rem 0.5rem;
/*em单位简直毫不兼容且毫无预测性*/
/*连ex这种东西都设计出来了 就不能设计一个e(任意字符)吗? */
/*最终在icon用px+硬编码数字 懒得折腾了*/
/*真空球形鸡设计*/
}
#index-table thead th {
font-weight: 800;
text-transform: uppercase;
/*英文标题大写, 虽然我用中文*/
letter-spacing: 0.5px;
}
#index-table tbody th,
#index-table tbody td {
text-align: left;
border-bottom: 1px solid var(--background-darker);
}
#index-table tr:hover {
background: var(--background-darker);
}
#index-table th:nth-child(1) {
min-width: 2.5em;
padding: 0.25em 0;
}
#index-table td:nth-child(1) {
width: 1.5em;
padding: 0.25em 0;
}
#index-table td:nth-child(2) {
width: 0.5em;
padding: 0.25em 0em;
/*就一个'>'号*/
}
#index-table th:nth-child(2),
#index-table td:nth-child(3) {
max-width: 0;
/*允许压缩*/
min-width: 120px;
}
#index-table th:nth-child(3),
#index-table td:nth-child(4) {
width: auto
/*能占就占满*/
}
#index-table th:nth-child(4) {
text-align: center;
}
#index-table td:nth-child(5) {
text-align: right;
width: 3em;
}
#index-table th:nth-child(5) {
text-align: left;
}
#index-table td:nth-child(6) {
width: 12em;
}
#index-table th:last-child,
#index-table td:last-child {
text-align: right;
padding-right: 2em;
max-width: 0;
/*允许压缩*/
min-width: 4em;
}
.placeholder {
flex: 1;
}
footer {
position: sticky;
bottom: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 12px 4px 12px;
background: var(--background);
border-top: 1px solid var(--background-verydarker);
font-size: 12px;
color: var(--foreground);
z-index: 100;
}
@keyframes fade {
0% {
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.pulse {
animation: fade 5s 1;
}
.item-selected::before {
content: ">";
justify-content: center;
align-items: center;
font-size: 12px;
color: var(--accent);
font-weight: bold;
}
.highlight {
background-color: var(--accent-lighten);
}
#drop-grid {
align-items: center;
display: none;
justify-content: center;
position: fixed;
top: 0px;
bottom: 0px;
right: 0px;
left: 0px;
z-index: 999;
border: 5px dashed var(--accent);
margin: 0px;
border-radius: 5px;
text-align: center;
font-size: 4em;
color: var(--accent);
opacity: 1;
background-color: rgba(255, 255, 255, 0.6);
padding: 20px;
}
#drop-grid::after {
content: "释放光标以传输文件";
color: var(--foreground);
background-color: rgba(255, 255, 255, 0.6);
text-align: center;
}
#notification {
display: flex;
flex-direction: row;
padding: 0 0;
}
#notification * {
padding: 0 0;
}
#statbar {
display: flex;
flex-direction: row;
padding: 0 0;
}
#statbar * {
padding: 0 0;
}
.nav:hover {
color: var(--accent);
}
.item-icon {
height: 16px;
width: auto;
/*垃圾css*/
cursor: pointer;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.type-icon-folder {
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNzc0NjkwMDA5MjU1IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjEwNTMwIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiPjxwYXRoIGQ9Ik04ODAgMjk4LjRINTIxTDQwMy43IDE4Ni4yYy0xLjUtMS40LTMuNS0yLjItNS41LTIuMkgxNDRjLTE3LjcgMC0zMiAxNC4zLTMyIDMydjU5MmMwIDE3LjcgMTQuMyAzMiAzMiAzMmg3MzZjMTcuNyAwIDMyLTE0LjMgMzItMzJWMzMwLjRjMC0xNy43LTE0LjMtMzItMzItMzJ6TTg0MCA3NjhIMTg0VjI1NmgxODguNWwxMTkuNiAxMTQuNEg4NDBWNzY4eiIgcC1pZD0iMTA1MzEiPjwvcGF0aD48L3N2Zz4=") !important;
}
.type-icon-file {
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/PjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+PHN2ZyB0PSIxNzc0Njg5OTcyMTU2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjUzNDAiIGRhdGEtc3BtLWFuY2hvci1pZD0iYTMxM3guc2VhcmNoX2luZGV4LjAuaTEuM2YwMTNhODFMUkJncXMiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PHBhdGggZD0iTTg1NC42IDI4OC42TDYzOS40IDczLjRjLTYtNi0xNC4xLTkuNC0yMi42LTkuNEgxOTJjLTE3LjcgMC0zMiAxNC4zLTMyIDMydjgzMmMwIDE3LjcgMTQuMyAzMiAzMiAzMmg2NDBjMTcuNyAwIDMyLTE0LjMgMzItMzJWMzExLjNjMC04LjUtMy40LTE2LjctOS40LTIyLjd6TTc5MC4yIDMyNkg2MDJWMTM3LjhMNzkwLjIgMzI2eiBtMS44IDU2MkgyMzJWMTM2aDMwMnYyMTZjMCAyMy4yIDE4LjggNDIgNDIgNDJoMjE2djQ5NHoiIHAtaWQ9IjUzNDEiIGRhdGEtc3BtLWFuY2hvci1pZD0iYTMxM3guc2VhcmNoX2luZGV4LjAuaTIuM2YwMTNhODFMUkJncXMiPjwvcGF0aD48L3N2Zz4=");
}
#help-panel {
display: flex;
text-align: center;
width: 100%;
}
#help-panel h3 {
padding: 0.2em 0 0 0;
margin: 0;
}
#help-panel p {
padding: 0.3em 0 0.5em 0;
line-height: 1.5em;
}
#help-panel table {
border-collapse: collapse;
align-items: center;
width: 40em;
max-height: 10em;
min-width: 30em;
max-width: 50%;
margin: auto;
border: 4px solid var(--foreground);
}
#help-panel table td {
border: 2px solid var(--background-darker);
}
#help-panel table * {
padding: 0.3em 1em;
}

204
ui/ui.html Normal file
View File

@@ -0,0 +1,204 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
<link rel="preconnect" href="https://fonts.loli.net">
<link href="data:image/svg+xml;base64,favicon_will_be_here" rel="icon" type="image/svg+xml" />
<style type="text/css">
css_will_be_here
</style>
<script>
readonly = {{.Ro }}
extraPath = {{.ExtraPath }}.slice(0, -1)
js_will_be_here
</script>
</head>
<body>
<input type="file" id="clickupload" multiple style="display:none" />
<header>
<a href="/"><img class="logo-img" src="data:image/svg+xml;base64,favicon_will_be_here" /></a>
<span class="nav" id="nav">{{.Path}}</span>
<span id="path" style="display: none;" onclick="return afi.directcall.bread_click(event)">{{.Path}}</span>
<div id="operations">
{{if not .Ro}}
<a onclick="document.getElementById('clickupload').click()" class="operation">上传文件</a>
<a onclick="afi.directcall.exec_touch()" class="operation">新建文件</a>
<a onclick="afi.directcall.exec_mkdir()" class="operation">新建目录</a>
<a onclick="afi.directcall.exec_mv()" class="operation">移动对象</a>
<a onclick="afi.directcall.exec_rm()" class="operation">递归删除</a>
<a onclick="" class="operation">登出</a>
{{end}}
{{if .Ro}}
<a onclick="" class="operation">登录</a>
{{end}}
<a onclick="afi.utils.helpOn()" class="operation" id="help-toggle">帮助菜单</a>
</div>
</header>
<table id="index-table">
<thead>
<tr>
<th colspan="2">图标</th>
<th>名称</th>
<th><!--我是占位的--></th>
<th>大小</th>
<th>类型</th>
<th>备注</th>
</tr>
</thead>
<tbody>
{{range .RowsFolders}}
<tr>
<td>
<div class="item-icon type-icon-folder"></div>
</td>
<td>
<div class="item-selector"></div>
</td>
<td>
<div class="item-name"><a class="item-links" href="{{.Href}}/"><span
class="ellipsis">{{.Name}}</span></a></div>
</td>
<td><!--我是占位的--></td>
<td>
<div class="item-size">{{.Size}}</div>
</td>
<td>
<div class="item-type">{{.Mime}}</div>
</td>
<td>
<div class="item-desc"><span class="ellipsis">{{.Desc}}</span></div>
</td>
</tr>
{{end}}
{{range .RowsFiles}}
<tr>
<td>
<div class="item-icon type-icon-file type-icon-{{.Ext}}"></div>
</td>
<td>
<div class="item-selector"></div>
</td>
<td>
<div class="item-name"><a class="item-links" href="{{.Href}}" download><span
class="ellipsis">{{.Name}}</span></a>
</div>
</td>
<td><!--我是占位的--></td>
<td>
<div class="item-size">{{.Size}}</div>
</td>
<td>
<div class="item-type">{{.Mime}}</div>
</td>
<td>
<div class="item-desc"><span class="ellipsis">{{.Desc}}</span></div>
</td>
</tr>
{{end}}
</tbody>
</table>
<div style="display: none;" onclick="afi.utils.helpOff()" id="help-panel">
<br>
<h3>帮助信息与按键绑定</h3>
<p>^[X] 表示组合键 Ctrl + [X] 或 Meta + [X]</p>
<table>
<tbody>
<tr>
<td>使用键盘浏览文件系统</td>
<td>方向键与回车</td>
</tr>
<tr>
<td>上传项目</td>
<td>拖放外部文件或文件夹到窗口内部;<br>单击按钮或 ^U / Shift + U 以呼出文件上传菜单</td>
</tr>
<tr>
<td>移动项目</td>
<td>直接拖放界面内的项目;<br>对选中项 ^X, 并在目标目录中 ^V</td>
</tr>
<tr>
<td>下载为归档文件(.zip)</td>
<td>Ctrl + 单击任一项目;<br>对选中项 ^↵</td>
</tr>
<tr>
<td>复制链接</td>
<td>右键任一项目复制;<br>对选中项 ^C</td>
</tr>
<tr>
<td>重命名项目</td>
<td>单击任一文件图标;<br>对选中项 ^E</td>
</tr>
<tr>
<td>删除项目</td>
<td>双击任一文件图标;<br>对选中项 ^⌫</td>
</tr>
<tr>
<td>新建文件夹</td>
<td>^M</td>
</tr>
<tr>
<td>复制多种校验和</td>
<td>对选中项目 ^Z</td>
</tr>
<tr>
<td>进行模糊搜索匹配</td>
<td>单独输入其他任意内容</td>
</tr>
</tbody>
</table>
<br>
<h3>关于 AFI</h3>
<p>AFI (敏捷文件索引器) 是 Go 编写的轻量级文件服务器.<br>
AFI 是基于 Gossa 的现代化改进与维护分支, 以 MIT 协议在<a href="">此处</a>开放源代码.<br>
您可在仓库网页中的 Issues 内反馈缺陷, 提出需求或想法 ^v^</p>
</div>
<div onclick="afi.sumsOff()" style="display: none;" id="sums">
<table id="sumsTable">
<thead>
<tr>
<td>按键</td>
<td>哈希算法</td>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>复制 sha1 校验和</td>
</tr>
<tr>
<td>2</td>
<td>复制 sha256 校验和</td>
</tr>
<tr>
<td>3</td>
<td>复制 sha512 校验和</td>
</tr>
<tr>
<td>5</td>
<td>复制 md5 校验和</td>
</tr>
</tbody>
</table>
</div>
<div class="placeholder"><!--我是液压机--></div>
<div id="drop-grid"></div>
<footer>
<div id="about">AFI - 轻巧敏捷的文件服务器</div>
<div style="display: inline-block;">
<div id="notification" style="display: inline-block;">
<span style="display: none;" id="badge"></span>
</div>
<div id="statbar" style="display: inline-block;">
<span id="bar-proc"></span>
<span id="bar-display" style="display: none;"></span>
<span id="bar-checksum" style="display: none;"></span>
</div>
<div id="timestamp" style="display: inline-block;">页面生成于 {{.TimeStamp}}</div>
</div>
</footer>
</body>
</html>