agent/sub_agent/static/file_manager/app.js

900 lines
33 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
const API_BASE = '/api/gui/files';
const EDITOR_PAGE = '/file-manager/editor';
const state = {
currentPath: '',
items: [],
selected: new Set(),
lastSelectedIndex: null,
clipboard: null, // {mode: 'copy'|'cut', items: []}
treeCache: new Map(),
treeExpanded: new Set(['']),
isDraggingSelection: false,
dragStart: null,
selectionRect: null,
selectionJustFinished: false,
selectionDisabled: false,
};
const icons = {
directory: '📁',
default: '📄',
editable: '📝',
code: '💻',
markdown: '🧾',
image: '🖼️',
archive: '🗃️',
};
const fileGrid = document.getElementById('fileGrid');
const directoryTree = document.getElementById('directoryTree');
const breadcrumbEl = document.getElementById('breadcrumb');
const selectionInfo = document.getElementById('selectionInfo');
const statusBar = document.getElementById('statusBar');
const contextMenu = document.getElementById('contextMenu');
const dialogBackdrop = document.getElementById('dialogBackdrop');
const dialogTitle = document.getElementById('dialogTitle');
const dialogContent = document.getElementById('dialogContent');
const dialogCancel = document.getElementById('dialogCancel');
const dialogConfirm = document.getElementById('dialogConfirm');
const hiddenUploader = document.getElementById('hiddenUploader');
const pasteBtn = document.getElementById('btnPaste');
const newFolderBtn = document.getElementById('btnNewFolder');
const newFileBtn = document.getElementById('btnNewFile');
const refreshBtn = document.getElementById('btnRefresh');
const uploadBtn = document.getElementById('btnUpload');
const backBtn = document.getElementById('btnBack');
const returnChatBtn = document.getElementById('btnReturnChat');
const downloadBtn = document.getElementById('btnDownload');
const renameBtn = document.getElementById('btnRename');
const copyBtn = document.getElementById('btnCopy');
const cutBtn = document.getElementById('btnCut');
const deleteBtn = document.getElementById('btnDelete');
const toggleSelectionBtn = document.getElementById('btnToggleSelection');
const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
const urlParams = new URLSearchParams(window.location.search);
const initialPathParam = (urlParams.get('path') || '').replace(/^\//, '').replace(/\/$/, '');
dialogBackdrop.hidden = true;
let dialogHandlers = { confirm: null, cancel: null };
function clearDialogHandlers() {
dialogHandlers.confirm = null;
dialogHandlers.cancel = null;
}
function registerDialogHandlers(confirmHandler, cancelHandler) {
dialogHandlers.confirm = confirmHandler || null;
dialogHandlers.cancel = cancelHandler || null;
}
function closeDialog() {
dialogBackdrop.hidden = true;
clearDialogHandlers();
}
dialogCancel.addEventListener('click', () => {
if (dialogHandlers.cancel) {
const handler = dialogHandlers.cancel;
clearDialogHandlers();
handler();
} else {
closeDialog();
}
});
dialogConfirm.addEventListener('click', () => {
if (dialogHandlers.confirm) {
const handler = dialogHandlers.confirm;
clearDialogHandlers();
handler();
} else {
closeDialog();
}
});
dialogBackdrop.addEventListener('click', (event) => {
if (event.target === dialogBackdrop) {
if (dialogHandlers.cancel) {
const handler = dialogHandlers.cancel;
clearDialogHandlers();
handler();
} else {
closeDialog();
}
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !dialogBackdrop.hidden) {
if (dialogHandlers.cancel) {
const handler = dialogHandlers.cancel;
clearDialogHandlers();
handler();
} else {
closeDialog();
}
}
});
closeDialog();
function showStatus(message) {
statusBar.textContent = message;
}
function formatSize(size) {
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(1)} MB`;
return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`;
}
function formatTime(ts) {
const d = new Date(ts * 1000);
return d.toLocaleString();
}
function joinPath(base, name) {
if (!base) return name;
return `${base.replace(/\/$/, '')}/${name}`;
}
function getIcon(entry) {
if (entry.type === 'directory') return icons.directory;
if (entry.is_editable) return icons.editable;
const ext = entry.extension || '';
if (['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp'].includes(ext)) {
return icons.image;
}
if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
return icons.archive;
}
if (['.js', '.ts', '.py', '.rb', '.php', '.java', '.kt', '.go', '.rs', '.c', '.cpp', '.h', '.hpp'].includes(ext)) {
return icons.code;
}
if (['.md', '.markdown'].includes(ext)) {
return icons.markdown;
}
return icons.default;
}
async function request(url, options = {}) {
const response = await fetch(url, options);
const data = await response.json().catch(() => ({}));
if (!response.ok || data.success === false) {
const message = data.error || data.message || `请求失败 (${response.status})`;
throw new Error(message);
}
return data;
}
function updateUrl(path) {
const url = new URL(window.location.href);
if (path) {
url.searchParams.set('path', path);
} else {
url.searchParams.delete('path');
}
window.history.replaceState({}, '', url.pathname + url.search);
}
async function ensureAncestors(path) {
await ensureTreeNode('', false);
state.treeExpanded.add('');
if (!path) {
renderTree();
return;
}
const segments = path.split('/').filter(Boolean);
let current = '';
for (let index = 0; index < segments.length; index += 1) {
const segment = segments[index];
current = current ? `${current}/${segment}` : segment;
if (index < segments.length - 1) {
state.treeExpanded.add(current);
}
await ensureTreeNode(current, false);
}
renderTree();
}
async function loadDirectory(path = '', { updateHistory = true } = {}) {
hideContextMenu();
showStatus('加载中...');
try {
const result = await request(`${API_BASE}/entries?path=${encodeURIComponent(path)}`);
const resolvedPath = result.data.path || '';
state.currentPath = resolvedPath;
state.items = result.data.items || [];
const directoryEntries = state.items.filter((item) => item.type === 'directory');
state.treeCache.set(resolvedPath, directoryEntries);
if (updateHistory) {
updateUrl(resolvedPath);
}
await ensureAncestors(resolvedPath);
renderBreadcrumb(result.data.breadcrumb || []);
renderGrid();
updateSelection([]);
state.lastSelectedIndex = null;
showStatus(`已加载 ${state.items.length}`);
} catch (err) {
showStatus(err.message);
}
}
function renderBreadcrumb(crumbs) {
breadcrumbEl.innerHTML = '';
crumbs.forEach((crumb, index) => {
const span = document.createElement('span');
span.textContent = crumb.name;
span.dataset.path = crumb.path;
span.addEventListener('click', () => {
loadDirectory(crumb.path);
});
breadcrumbEl.appendChild(span);
if (index < crumbs.length - 1) {
const sep = document.createElement('span');
sep.textContent = '';
sep.classList.add('fm-breadcrumb-sep');
breadcrumbEl.appendChild(sep);
}
});
}
function renderGrid() {
fileGrid.innerHTML = '';
state.items.forEach((entry, index) => {
const card = document.createElement('div');
card.className = 'fm-card';
card.tabIndex = 0;
card.dataset.path = entry.path;
card.dataset.index = index;
if (state.selected.has(entry.path)) {
card.classList.add('selected');
}
const icon = document.createElement('div');
icon.className = 'fm-card-icon';
icon.textContent = getIcon(entry);
const name = document.createElement('div');
name.className = 'fm-card-name';
name.textContent = entry.name;
const meta = document.createElement('div');
meta.className = 'fm-card-meta';
const lines = [];
if (entry.type === 'file') {
lines.push(formatSize(entry.size));
} else {
lines.push('目录');
}
lines.push(formatTime(entry.modified_at));
meta.innerHTML = lines.join('<br>');
card.appendChild(icon);
card.appendChild(name);
card.appendChild(meta);
card.addEventListener('click', (event) => handleItemClick(event, entry, index));
card.addEventListener('dblclick', () => handleItemDoubleClick(entry));
card.addEventListener('contextmenu', (event) => handleItemContextMenu(event, entry));
fileGrid.appendChild(card);
});
const overlay = document.createElement('div');
overlay.className = 'fm-drop-overlay';
overlay.textContent = '释放即可上传到此目录';
fileGrid.appendChild(overlay);
}
function updateSelection(paths, options = { append: false, range: false }) {
if (!options.append && !options.range) {
state.selected.clear();
if (paths.length === 0) {
state.lastSelectedIndex = null;
}
}
paths.forEach((path) => {
if (state.selected.has(path) && options.append) {
state.selected.delete(path);
} else {
state.selected.add(path);
}
});
syncSelectionUI();
}
function syncSelectionUI() {
const cards = fileGrid.querySelectorAll('.fm-card');
cards.forEach((card) => {
if (state.selected.has(card.dataset.path)) {
card.classList.add('selected');
} else {
card.classList.remove('selected');
}
});
selectionInfo.textContent = `已选中 ${state.selected.size}`;
pasteBtn.disabled = !state.clipboard || !state.clipboard.items.length;
}
function handleItemClick(event, entry, index) {
const isMetaKey = event.metaKey || event.ctrlKey;
const isShiftKey = event.shiftKey;
if (isShiftKey && state.lastSelectedIndex !== null) {
const start = Math.min(state.lastSelectedIndex, index);
const end = Math.max(state.lastSelectedIndex, index);
const paths = state.items.slice(start, end + 1).map((item) => item.path);
updateSelection(paths, { range: true });
} else if (isMetaKey) {
updateSelection([entry.path], { append: true });
state.lastSelectedIndex = index;
} else {
updateSelection([entry.path], { append: false });
state.lastSelectedIndex = index;
}
}
function handleItemDoubleClick(entry) {
if (entry.type === 'directory') {
loadDirectory(entry.path);
return;
}
if (entry.is_editable) {
window.location.href = `${EDITOR_PAGE}?path=${encodeURIComponent(entry.path)}`;
return;
}
window.open(`${API_BASE}/download?path=${encodeURIComponent(entry.path)}`, '_blank');
}
function handleItemContextMenu(event, entry) {
event.preventDefault();
if (!state.selected.has(entry.path)) {
updateSelection([entry.path], { append: false });
}
showContextMenu(event.clientX, event.clientY);
}
function showContextMenu(x, y) {
const single = state.selected.size === 1;
const singleEntry = single ? getSingleSelected() : null;
contextMenu.innerHTML = '';
const entries = [];
if (singleEntry) {
if (singleEntry.type === 'directory') {
entries.push({ label: '打开', action: openSelected, disabled: false });
} else if (singleEntry.is_editable) {
entries.push({ label: '在编辑器中打开', action: openEditor, disabled: false });
if (singleEntry.extension === '.html' || singleEntry.extension === '.htm') {
entries.push({ label: '预览', action: previewSelected, disabled: false });
}
entries.push({ label: '下载', action: downloadSelected, disabled: false });
} else {
const isHtml = singleEntry.extension === '.html' || singleEntry.extension === '.htm';
if (isHtml) {
entries.push({ label: '预览', action: previewSelected, disabled: false });
}
entries.push({ label: '下载', action: downloadSelected, disabled: false });
}
} else if (state.selected.size > 0) {
entries.push({ label: '下载', action: downloadSelected, disabled: false });
}
entries.push(
{ label: '重命名', action: renameSelected, disabled: !single },
{ label: '复制', action: copySelected, disabled: state.selected.size === 0 },
{ label: '剪切', action: cutSelected, disabled: state.selected.size === 0 },
{ label: '粘贴', action: pasteClipboard, disabled: !state.clipboard || !state.clipboard.items.length },
{ label: '删除', action: deleteSelected, disabled: state.selected.size === 0 },
);
entries.forEach((item) => {
const btn = document.createElement('button');
btn.textContent = item.label;
btn.disabled = item.disabled;
btn.addEventListener('click', () => {
hideContextMenu();
item.action();
});
contextMenu.appendChild(btn);
});
contextMenu.style.display = 'block';
const { innerWidth, innerHeight } = window;
const menuRect = contextMenu.getBoundingClientRect();
const left = clamp(x, 0, innerWidth - menuRect.width);
const top = clamp(y, 0, innerHeight - menuRect.height);
contextMenu.style.left = `${left}px`;
contextMenu.style.top = `${top}px`;
}
function hideContextMenu() {
contextMenu.style.display = 'none';
}
function getSingleSelected() {
if (state.selected.size !== 1) return null;
const path = Array.from(state.selected)[0];
return state.items.find((item) => item.path === path) || null;
}
function openSelected() {
const entry = getSingleSelected();
if (!entry) return;
handleItemDoubleClick(entry);
}
function previewSelected() {
const entry = getSingleSelected();
if (!entry) return;
if (entry.extension !== '.html' && entry.extension !== '.htm') {
showStatus('仅支持预览 HTML 文件');
return;
}
window.open(`/file-preview/${encodeURIComponent(entry.path)}`, '_blank');
}
function openEditor() {
const entry = getSingleSelected();
if (!entry || !entry.is_editable) return;
window.location.href = `${EDITOR_PAGE}?path=${encodeURIComponent(entry.path)}`;
}
function triggerBlobDownload(filename, blob) {
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = filename;
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
URL.revokeObjectURL(url);
}
async function downloadSelected() {
if (!state.selected.size) return;
if (state.selected.size === 1) {
const path = Array.from(state.selected)[0];
window.open(`${API_BASE}/download?path=${encodeURIComponent(path)}`, '_blank');
return;
}
try {
const resp = await fetch(`${API_BASE}/download/batch`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paths: Array.from(state.selected) })
});
if (!resp.ok) {
let message = '批量下载失败';
try {
const data = await resp.json();
message = data.error || data.message || message;
} catch (_) {
// ignore
}
throw new Error(message);
}
const blob = await resp.blob();
triggerBlobDownload(`selected_${Date.now()}.zip`, blob);
showStatus(`已开始下载 ${state.selected.size} 个项目`);
} catch (err) {
showStatus(err.message);
}
}
async function renameSelected() {
const entry = getSingleSelected();
if (!entry) return;
const newName = await promptDialog('重命名', entry.name);
if (!newName) return;
try {
await request(`${API_BASE}/rename`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: entry.path, new_name: newName }),
});
await loadDirectory(state.currentPath);
} catch (err) {
showStatus(err.message);
}
}
function copySelected() {
if (!state.selected.size) return;
state.clipboard = { mode: 'copy', items: Array.from(state.selected) };
showStatus(`已复制 ${state.clipboard.items.length}`);
syncSelectionUI();
}
function cutSelected() {
if (!state.selected.size) return;
state.clipboard = { mode: 'cut', items: Array.from(state.selected) };
showStatus(`已剪切 ${state.clipboard.items.length}`);
syncSelectionUI();
}
async function pasteClipboard() {
if (!state.clipboard || !state.clipboard.items.length) return;
const endpoint = state.clipboard.mode === 'copy' ? 'copy' : 'move';
try {
await request(`${API_BASE}/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
paths: state.clipboard.items,
target_dir: state.currentPath,
}),
});
if (state.clipboard.mode === 'cut') {
state.clipboard = null;
}
await loadDirectory(state.currentPath);
} catch (err) {
showStatus(err.message);
}
}
async function deleteSelected() {
if (!state.selected.size) return;
const confirm = await confirmDialog(`确认删除选中的 ${state.selected.size} 项吗?该操作不可撤销。`);
if (!confirm) return;
try {
await request(`${API_BASE}/delete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paths: Array.from(state.selected) }),
});
await loadDirectory(state.currentPath);
} catch (err) {
showStatus(err.message);
}
}
function promptDialog(title, defaultValue = '') {
return new Promise((resolve) => {
dialogTitle.textContent = title;
dialogContent.innerHTML = '';
const input = document.createElement('input');
input.value = defaultValue;
input.autofocus = true;
dialogContent.appendChild(input);
const finish = (value) => {
closeDialog();
resolve(value);
};
registerDialogHandlers(() => finish(input.value.trim()), () => finish(null));
dialogBackdrop.hidden = false;
input.addEventListener('keydown', (evt) => {
if (evt.key === 'Enter') {
evt.preventDefault();
finish(input.value.trim());
} else if (evt.key === 'Escape') {
evt.preventDefault();
finish(null);
}
});
setTimeout(() => input.select(), 50);
});
}
function confirmDialog(message) {
return new Promise((resolve) => {
dialogTitle.textContent = '确认操作';
dialogContent.innerHTML = `<p>${message}</p>`;
const finish = (value) => {
closeDialog();
resolve(value);
};
registerDialogHandlers(() => finish(true), () => finish(false));
dialogBackdrop.hidden = false;
});
}
function handleGlobalClick(event) {
if (!contextMenu.contains(event.target)) {
hideContextMenu();
}
}
function handleGridBackgroundClick(event) {
if (event.target === fileGrid) {
if (state.selectionJustFinished) {
return;
}
updateSelection([]);
}
}
function handleDragEnter(event) {
event.preventDefault();
fileGrid.classList.add('drop-target');
}
function handleDragOver(event) {
event.preventDefault();
}
function handleDragLeave(event) {
if (event.target === fileGrid) {
fileGrid.classList.remove('drop-target');
}
}
async function handleDrop(event) {
event.preventDefault();
fileGrid.classList.remove('drop-target');
if (!event.dataTransfer || !event.dataTransfer.files.length) return;
const files = event.dataTransfer.files;
await uploadFiles(files, state.currentPath);
}
async function uploadFiles(fileList, targetPath) {
for (const file of fileList) {
const form = new FormData();
form.append('file', file, file.name);
form.append('filename', file.name);
form.append('path', targetPath);
try {
await request(`${API_BASE}/upload`, {
method: 'POST',
body: form,
});
showStatus(`已上传 ${file.name}`);
} catch (err) {
showStatus(`上传失败:${err.message}`);
}
}
await loadDirectory(state.currentPath);
}
function initSelectionRectangle() {
fileGrid.addEventListener('pointerdown', (event) => {
if (state.selectionDisabled) return;
if (event.target !== fileGrid) return;
state.isDraggingSelection = true;
state.dragStart = { x: event.clientX, y: event.clientY };
state.selectionRect = document.createElement('div');
state.selectionRect.className = 'fm-selection-rect';
fileGrid.appendChild(state.selectionRect);
updateSelection([]);
state.selectionJustFinished = false;
fileGrid.setPointerCapture(event.pointerId);
});
fileGrid.addEventListener('pointermove', (event) => {
if (!state.isDraggingSelection || !state.selectionRect) return;
const rect = fileGrid.getBoundingClientRect();
const current = { x: event.clientX, y: event.clientY };
const x = Math.min(state.dragStart.x, current.x) - rect.left + fileGrid.scrollLeft;
const y = Math.min(state.dragStart.y, current.y) - rect.top + fileGrid.scrollTop;
const width = Math.abs(state.dragStart.x - current.x);
const height = Math.abs(state.dragStart.y - current.y);
Object.assign(state.selectionRect.style, {
left: `${x}px`,
top: `${y}px`,
width: `${width}px`,
height: `${height}px`,
});
const selectionBox = {
left: Math.min(state.dragStart.x, current.x),
right: Math.max(state.dragStart.x, current.x),
top: Math.min(state.dragStart.y, current.y),
bottom: Math.max(state.dragStart.y, current.y),
};
const selected = [];
const cards = fileGrid.querySelectorAll('.fm-card');
cards.forEach((card) => {
const bounds = card.getBoundingClientRect();
const intersects = !(selectionBox.right < bounds.left ||
selectionBox.left > bounds.right ||
selectionBox.bottom < bounds.top ||
selectionBox.top > bounds.bottom);
if (intersects) {
selected.push(card.dataset.path);
}
});
updateSelection(selected);
});
fileGrid.addEventListener('pointerup', (event) => {
if (!state.isDraggingSelection) return;
state.isDraggingSelection = false;
if (state.selectionRect) {
state.selectionRect.remove();
state.selectionRect = null;
}
state.selectionJustFinished = true;
requestAnimationFrame(() => {
state.selectionJustFinished = false;
});
fileGrid.releasePointerCapture(event.pointerId);
});
}
async function ensureTreeNode(path, shouldRender = true) {
let changed = false;
if (!state.treeCache.has(path)) {
try {
const result = await request(`${API_BASE}/entries?path=${encodeURIComponent(path)}`);
const directories = result.data.items.filter((item) => item.type === 'directory');
state.treeCache.set(path, directories);
changed = true;
} catch (err) {
showStatus(err.message);
}
}
if (shouldRender && changed) {
renderTree();
}
return changed;
}
function renderTree() {
directoryTree.innerHTML = '';
const rootNode = createTreeNode('', '根目录');
directoryTree.appendChild(rootNode);
}
function createTreeNode(path, name) {
const li = document.createElement('li');
const header = document.createElement('div');
header.className = 'fm-tree-item';
if (path === state.currentPath) {
header.classList.add('active');
}
const toggle = document.createElement('span');
toggle.className = 'fm-tree-toggle';
toggle.textContent = state.treeExpanded.has(path) ? '▾' : '▸';
toggle.addEventListener('click', async (event) => {
event.stopPropagation();
if (state.treeExpanded.has(path)) {
state.treeExpanded.delete(path);
} else {
state.treeExpanded.add(path);
await ensureTreeNode(path, false);
}
renderTree();
});
const label = document.createElement('span');
label.textContent = name;
label.addEventListener('click', () => loadDirectory(path));
header.addEventListener('click', () => loadDirectory(path));
header.appendChild(toggle);
header.appendChild(label);
li.appendChild(header);
if (state.treeExpanded.has(path)) {
const children = document.createElement('ul');
children.className = 'fm-tree-children';
const dirs = state.treeCache.get(path) || [];
dirs.forEach((dir) => {
const child = createTreeNode(dir.path, dir.name);
children.appendChild(child);
});
li.appendChild(children);
}
return li;
}
function bindToolbar() {
newFolderBtn.addEventListener('click', async () => {
const name = await promptDialog('新建文件夹', '新建文件夹');
if (!name) return;
try {
await request(`${API_BASE}/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: state.currentPath,
name,
type: 'directory',
}),
});
await loadDirectory(state.currentPath);
} catch (err) {
showStatus(err.message);
}
});
newFileBtn.addEventListener('click', async () => {
const name = await promptDialog('新建文件', '新建文件.txt');
if (!name) return;
try {
await request(`${API_BASE}/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
path: state.currentPath,
name,
type: 'file',
}),
});
await loadDirectory(state.currentPath);
} catch (err) {
showStatus(err.message);
}
});
refreshBtn.addEventListener('click', () => loadDirectory(state.currentPath));
uploadBtn.addEventListener('click', () => hiddenUploader.click());
backBtn.addEventListener('click', () => {
if (!state.currentPath) {
loadDirectory('');
return;
}
const segments = state.currentPath.split('/').filter(Boolean);
if (segments.length === 0) {
loadDirectory('');
return;
}
segments.pop();
const parentPath = segments.join('/');
loadDirectory(parentPath);
});
returnChatBtn.addEventListener('click', () => {
window.location.href = '/new';
});
downloadBtn.addEventListener('click', downloadSelected);
renameBtn.addEventListener('click', renameSelected);
copyBtn.addEventListener('click', copySelected);
cutBtn.addEventListener('click', cutSelected);
pasteBtn.addEventListener('click', pasteClipboard);
deleteBtn.addEventListener('click', deleteSelected);
toggleSelectionBtn.addEventListener('click', () => {
state.selectionDisabled = !state.selectionDisabled;
toggleSelectionBtn.textContent = state.selectionDisabled ? '启用框选' : '禁用框选';
const msg = state.selectionDisabled ? '已禁用框选' : '已启用框选';
showStatus(msg);
});
hiddenUploader.addEventListener('change', async (event) => {
const files = event.target.files;
if (files && files.length) {
await uploadFiles(files, state.currentPath);
}
hiddenUploader.value = '';
});
}
function bindGlobalEvents() {
document.addEventListener('click', handleGlobalClick);
fileGrid.addEventListener('click', handleGridBackgroundClick);
fileGrid.addEventListener('contextmenu', (event) => {
if (event.target === fileGrid) {
event.preventDefault();
if (state.selected.size) {
showContextMenu(event.clientX, event.clientY);
}
}
});
fileGrid.addEventListener('dragenter', handleDragEnter);
fileGrid.addEventListener('dragover', handleDragOver);
fileGrid.addEventListener('dragleave', handleDragLeave);
fileGrid.addEventListener('drop', handleDrop);
initSelectionRectangle();
}
async function bootstrap() {
bindToolbar();
bindGlobalEvents();
await loadDirectory(initialPathParam, { updateHistory: false });
}
bootstrap().catch((err) => {
console.error(err);
showStatus(err.message);
});
})();