Replace file manager toolbar buttons with hamburger menu
Removed individual delete, upload, hidden files, and refresh buttons. Added a hamburger (☰) menu with: - Select: toggles selection mode with checkboxes for multi-select - Delete: enabled only when files are selected - Upload: file upload - Refresh: reload current directory - Show Hidden Files: toggles dotfile visibility, persisted via localStorage Toolbar now only has New File, New Folder, and the hamburger menu for a cleaner interface. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
30f0536b00
commit
3c295ba302
2 changed files with 180 additions and 68 deletions
|
|
@ -76,9 +76,8 @@
|
|||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.files-action-btn.active {
|
||||
background: var(--accent-hover);
|
||||
color: var(--accent);
|
||||
.files-menu-btn {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* File body — dual pane */
|
||||
|
|
@ -264,3 +263,27 @@
|
|||
background: var(--border-structural);
|
||||
margin: 4px 8px;
|
||||
}
|
||||
|
||||
.context-item.disabled {
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.context-item.disabled:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/* Selection mode checkbox */
|
||||
.file-select-cb {
|
||||
font-size: 14px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Toolbar menu — right-aligned dropdown */
|
||||
.files-toolbar-menu {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
let selectedFiles = new Set();
|
||||
let contextMenuEl = null;
|
||||
let uploadInput = null;
|
||||
let showHidden = false; // show dotfiles
|
||||
let showHidden = localStorage.getItem('atlus_files_show_hidden') === '1';
|
||||
let selectionMode = false;
|
||||
let lastEntries = []; // cached for re-render without refetch
|
||||
|
||||
const FILE_ICONS = {
|
||||
dir: '📁',
|
||||
|
|
@ -66,8 +68,8 @@
|
|||
try {
|
||||
const res = await Atlus.apiFetch(`/api/files/list?path=${encodeURIComponent(path)}`);
|
||||
if (!res.ok) throw new Error('Failed to load directory');
|
||||
const entries = await res.json();
|
||||
renderFileList(entries);
|
||||
lastEntries = await res.json();
|
||||
renderFileList(lastEntries);
|
||||
} catch (e) {
|
||||
fileListEl.innerHTML = `<div style="padding:16px;color:var(--status-red);font-family:var(--font-mono);font-size:13px;">Error: ${e.message}</div>`;
|
||||
}
|
||||
|
|
@ -108,38 +110,61 @@
|
|||
row.dataset.path = entry.path;
|
||||
|
||||
const icon = getFileIcon(entry);
|
||||
row.innerHTML = `
|
||||
<div class="file-name">
|
||||
<span class="file-icon ${entry.is_dir ? 'dir' : ''}">${icon}</span>
|
||||
<span>${entry.name}</span>
|
||||
</div>
|
||||
<span class="file-size">${entry.is_dir ? '--' : formatSize(entry.size)}</span>
|
||||
<span class="file-modified">${formatDate(entry.modified)}</span>
|
||||
<span class="file-perms">${entry.permissions}</span>
|
||||
`;
|
||||
|
||||
// Single click — navigate dir or toggle selection
|
||||
row.addEventListener('click', () => {
|
||||
if (entry.is_dir) {
|
||||
loadDirectory(entry.path);
|
||||
} else {
|
||||
// Toggle selection
|
||||
row.classList.toggle('selected');
|
||||
if (selectionMode) {
|
||||
const checked = selectedFiles.has(entry.path);
|
||||
row.classList.toggle('selected', checked);
|
||||
row.innerHTML = `
|
||||
<div class="file-name">
|
||||
<span class="file-select-cb">${checked ? '☑' : '☐'}</span>
|
||||
<span class="file-icon ${entry.is_dir ? 'dir' : ''}">${icon}</span>
|
||||
<span>${entry.name}</span>
|
||||
</div>
|
||||
<span class="file-size">${entry.is_dir ? '--' : formatSize(entry.size)}</span>
|
||||
<span class="file-modified">${formatDate(entry.modified)}</span>
|
||||
<span class="file-perms">${entry.permissions}</span>
|
||||
`;
|
||||
row.addEventListener('click', () => {
|
||||
if (selectedFiles.has(entry.path)) {
|
||||
selectedFiles.delete(entry.path);
|
||||
} else {
|
||||
selectedFiles.add(entry.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
renderFileList(lastEntries);
|
||||
});
|
||||
} else {
|
||||
row.innerHTML = `
|
||||
<div class="file-name">
|
||||
<span class="file-icon ${entry.is_dir ? 'dir' : ''}">${icon}</span>
|
||||
<span>${entry.name}</span>
|
||||
</div>
|
||||
<span class="file-size">${entry.is_dir ? '--' : formatSize(entry.size)}</span>
|
||||
<span class="file-modified">${formatDate(entry.modified)}</span>
|
||||
<span class="file-perms">${entry.permissions}</span>
|
||||
`;
|
||||
|
||||
// Double click — open file in editor
|
||||
row.addEventListener('dblclick', (e) => {
|
||||
if (!entry.is_dir) {
|
||||
e.preventDefault();
|
||||
openInEditor(entry);
|
||||
}
|
||||
});
|
||||
// Single click — navigate dir or toggle selection
|
||||
row.addEventListener('click', () => {
|
||||
if (entry.is_dir) {
|
||||
loadDirectory(entry.path);
|
||||
} else {
|
||||
row.classList.toggle('selected');
|
||||
if (selectedFiles.has(entry.path)) {
|
||||
selectedFiles.delete(entry.path);
|
||||
} else {
|
||||
selectedFiles.add(entry.path);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Double click — open file in editor
|
||||
row.addEventListener('dblclick', (e) => {
|
||||
if (!entry.is_dir) {
|
||||
e.preventDefault();
|
||||
openInEditor(entry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Long-press for context menu
|
||||
let pressTimer;
|
||||
|
|
@ -187,7 +212,7 @@
|
|||
});
|
||||
}
|
||||
|
||||
// ---- Context menu ----
|
||||
// ---- Context menu (right-click on files) ----
|
||||
|
||||
function showContextMenu(e, entry) {
|
||||
hideContextMenu();
|
||||
|
|
@ -196,7 +221,7 @@
|
|||
|
||||
const items = [];
|
||||
|
||||
// If right-clicked on empty area or a specific entry
|
||||
// If right-clicked on a specific entry
|
||||
if (entry) {
|
||||
if (entry.is_dir) {
|
||||
items.push({ label: 'Open', action: () => loadDirectory(entry.path) });
|
||||
|
|
@ -254,6 +279,92 @@
|
|||
}
|
||||
}
|
||||
|
||||
// ---- Hamburger toolbar menu ----
|
||||
|
||||
let toolbarMenuEl = null;
|
||||
|
||||
function showToolbarMenu(anchorBtn) {
|
||||
hideToolbarMenu();
|
||||
|
||||
toolbarMenuEl = document.createElement('div');
|
||||
toolbarMenuEl.className = 'file-context-menu files-toolbar-menu';
|
||||
|
||||
const items = [
|
||||
{
|
||||
label: selectionMode ? '✓ Select' : 'Select',
|
||||
action: () => {
|
||||
selectionMode = !selectionMode;
|
||||
if (!selectionMode) selectedFiles.clear();
|
||||
renderFileList(lastEntries);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
disabled: selectedFiles.size === 0,
|
||||
danger: true,
|
||||
action: () => deleteSelected(),
|
||||
},
|
||||
{ sep: true },
|
||||
{
|
||||
label: 'Upload',
|
||||
action: () => uploadFile(),
|
||||
},
|
||||
{ sep: true },
|
||||
{
|
||||
label: 'Refresh',
|
||||
action: () => loadDirectory(currentPath),
|
||||
},
|
||||
{
|
||||
label: showHidden ? '✓ Show Hidden Files' : 'Show Hidden Files',
|
||||
action: () => {
|
||||
showHidden = !showHidden;
|
||||
localStorage.setItem('atlus_files_show_hidden', showHidden ? '1' : '0');
|
||||
renderFileList(lastEntries);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.sep) {
|
||||
const sep = document.createElement('div');
|
||||
sep.className = 'context-sep';
|
||||
toolbarMenuEl.appendChild(sep);
|
||||
return;
|
||||
}
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'context-item' + (item.danger ? ' danger' : '') + (item.disabled ? ' disabled' : '');
|
||||
btn.textContent = item.label;
|
||||
if (item.disabled) {
|
||||
btn.disabled = true;
|
||||
} else {
|
||||
btn.addEventListener('click', () => {
|
||||
hideToolbarMenu();
|
||||
item.action();
|
||||
});
|
||||
}
|
||||
toolbarMenuEl.appendChild(btn);
|
||||
});
|
||||
|
||||
// Position below the anchor button, right-aligned
|
||||
const rect = anchorBtn.getBoundingClientRect();
|
||||
toolbarMenuEl.style.right = (window.innerWidth - rect.right) + 'px';
|
||||
toolbarMenuEl.style.top = rect.bottom + 4 + 'px';
|
||||
toolbarMenuEl.style.left = 'auto';
|
||||
document.body.appendChild(toolbarMenuEl);
|
||||
|
||||
// Close on outside click
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', hideToolbarMenu, { once: true });
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function hideToolbarMenu() {
|
||||
if (toolbarMenuEl) {
|
||||
toolbarMenuEl.remove();
|
||||
toolbarMenuEl = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- File operations ----
|
||||
|
||||
async function createNewFile() {
|
||||
|
|
@ -302,6 +413,7 @@
|
|||
});
|
||||
}
|
||||
selectedFiles.clear();
|
||||
selectionMode = false;
|
||||
loadDirectory(currentPath);
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +507,7 @@
|
|||
newFolderBtn.addEventListener('click', createNewFolder);
|
||||
btnGroup.appendChild(newFolderBtn);
|
||||
|
||||
// Upload
|
||||
// Hidden upload input
|
||||
uploadInput = document.createElement('input');
|
||||
uploadInput.type = 'file';
|
||||
uploadInput.style.display = 'none';
|
||||
|
|
@ -407,41 +519,16 @@
|
|||
});
|
||||
container.appendChild(uploadInput);
|
||||
|
||||
const uploadBtn = document.createElement('button');
|
||||
uploadBtn.className = 'files-action-btn';
|
||||
uploadBtn.textContent = '⬆';
|
||||
uploadBtn.title = 'Upload';
|
||||
uploadBtn.addEventListener('click', uploadFile);
|
||||
btnGroup.appendChild(uploadBtn);
|
||||
|
||||
// Delete selected
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'files-action-btn';
|
||||
deleteBtn.textContent = '🗑';
|
||||
deleteBtn.title = 'Delete Selected';
|
||||
deleteBtn.addEventListener('click', deleteSelected);
|
||||
btnGroup.appendChild(deleteBtn);
|
||||
|
||||
// Toggle hidden files
|
||||
const hiddenBtn = document.createElement('button');
|
||||
hiddenBtn.className = 'files-action-btn files-hidden-toggle' + (showHidden ? ' active' : '');
|
||||
hiddenBtn.innerHTML = '<span style="font-size:20px;line-height:1;">.</span>';
|
||||
hiddenBtn.title = 'Show Hidden Files';
|
||||
hiddenBtn.addEventListener('click', () => {
|
||||
showHidden = !showHidden;
|
||||
hiddenBtn.classList.toggle('active', showHidden);
|
||||
hiddenBtn.title = showHidden ? 'Hide Hidden Files' : 'Show Hidden Files';
|
||||
loadDirectory(currentPath);
|
||||
// Hamburger menu button
|
||||
const menuBtn = document.createElement('button');
|
||||
menuBtn.className = 'files-action-btn files-menu-btn';
|
||||
menuBtn.innerHTML = '☰';
|
||||
menuBtn.title = 'Menu';
|
||||
menuBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
showToolbarMenu(menuBtn);
|
||||
});
|
||||
btnGroup.appendChild(hiddenBtn);
|
||||
|
||||
// Refresh
|
||||
const refreshBtn = document.createElement('button');
|
||||
refreshBtn.className = 'files-action-btn';
|
||||
refreshBtn.textContent = '↻';
|
||||
refreshBtn.title = 'Refresh';
|
||||
refreshBtn.addEventListener('click', () => loadDirectory(currentPath));
|
||||
btnGroup.appendChild(refreshBtn);
|
||||
btnGroup.appendChild(menuBtn);
|
||||
|
||||
toolbar.appendChild(btnGroup);
|
||||
container.appendChild(toolbar);
|
||||
|
|
@ -508,6 +595,7 @@
|
|||
|
||||
destroy() {
|
||||
hideContextMenu();
|
||||
hideToolbarMenu();
|
||||
container = null;
|
||||
fileListEl = null;
|
||||
breadcrumbEl = null;
|
||||
|
|
@ -515,6 +603,7 @@
|
|||
uploadInput = null;
|
||||
currentPath = '/';
|
||||
selectedFiles.clear();
|
||||
selectionMode = false;
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
Loading…
Reference in a new issue