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);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.files-action-btn.active {
|
.files-menu-btn {
|
||||||
background: var(--accent-hover);
|
font-size: 18px;
|
||||||
color: var(--accent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* File body — dual pane */
|
/* File body — dual pane */
|
||||||
|
|
@ -264,3 +263,27 @@
|
||||||
background: var(--border-structural);
|
background: var(--border-structural);
|
||||||
margin: 4px 8px;
|
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 selectedFiles = new Set();
|
||||||
let contextMenuEl = null;
|
let contextMenuEl = null;
|
||||||
let uploadInput = 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 = {
|
const FILE_ICONS = {
|
||||||
dir: '📁',
|
dir: '📁',
|
||||||
|
|
@ -66,8 +68,8 @@
|
||||||
try {
|
try {
|
||||||
const res = await Atlus.apiFetch(`/api/files/list?path=${encodeURIComponent(path)}`);
|
const res = await Atlus.apiFetch(`/api/files/list?path=${encodeURIComponent(path)}`);
|
||||||
if (!res.ok) throw new Error('Failed to load directory');
|
if (!res.ok) throw new Error('Failed to load directory');
|
||||||
const entries = await res.json();
|
lastEntries = await res.json();
|
||||||
renderFileList(entries);
|
renderFileList(lastEntries);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
fileListEl.innerHTML = `<div style="padding:16px;color:var(--status-red);font-family:var(--font-mono);font-size:13px;">Error: ${e.message}</div>`;
|
fileListEl.innerHTML = `<div style="padding:16px;color:var(--status-red);font-family:var(--font-mono);font-size:13px;">Error: ${e.message}</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -108,6 +110,29 @@
|
||||||
row.dataset.path = entry.path;
|
row.dataset.path = entry.path;
|
||||||
|
|
||||||
const icon = getFileIcon(entry);
|
const icon = getFileIcon(entry);
|
||||||
|
|
||||||
|
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 = `
|
row.innerHTML = `
|
||||||
<div class="file-name">
|
<div class="file-name">
|
||||||
<span class="file-icon ${entry.is_dir ? 'dir' : ''}">${icon}</span>
|
<span class="file-icon ${entry.is_dir ? 'dir' : ''}">${icon}</span>
|
||||||
|
|
@ -123,7 +148,6 @@
|
||||||
if (entry.is_dir) {
|
if (entry.is_dir) {
|
||||||
loadDirectory(entry.path);
|
loadDirectory(entry.path);
|
||||||
} else {
|
} else {
|
||||||
// Toggle selection
|
|
||||||
row.classList.toggle('selected');
|
row.classList.toggle('selected');
|
||||||
if (selectedFiles.has(entry.path)) {
|
if (selectedFiles.has(entry.path)) {
|
||||||
selectedFiles.delete(entry.path);
|
selectedFiles.delete(entry.path);
|
||||||
|
|
@ -140,6 +164,7 @@
|
||||||
openInEditor(entry);
|
openInEditor(entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Long-press for context menu
|
// Long-press for context menu
|
||||||
let pressTimer;
|
let pressTimer;
|
||||||
|
|
@ -187,7 +212,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Context menu ----
|
// ---- Context menu (right-click on files) ----
|
||||||
|
|
||||||
function showContextMenu(e, entry) {
|
function showContextMenu(e, entry) {
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
|
|
@ -196,7 +221,7 @@
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
// If right-clicked on empty area or a specific entry
|
// If right-clicked on a specific entry
|
||||||
if (entry) {
|
if (entry) {
|
||||||
if (entry.is_dir) {
|
if (entry.is_dir) {
|
||||||
items.push({ label: 'Open', action: () => loadDirectory(entry.path) });
|
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 ----
|
// ---- File operations ----
|
||||||
|
|
||||||
async function createNewFile() {
|
async function createNewFile() {
|
||||||
|
|
@ -302,6 +413,7 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
selectedFiles.clear();
|
selectedFiles.clear();
|
||||||
|
selectionMode = false;
|
||||||
loadDirectory(currentPath);
|
loadDirectory(currentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -395,7 +507,7 @@
|
||||||
newFolderBtn.addEventListener('click', createNewFolder);
|
newFolderBtn.addEventListener('click', createNewFolder);
|
||||||
btnGroup.appendChild(newFolderBtn);
|
btnGroup.appendChild(newFolderBtn);
|
||||||
|
|
||||||
// Upload
|
// Hidden upload input
|
||||||
uploadInput = document.createElement('input');
|
uploadInput = document.createElement('input');
|
||||||
uploadInput.type = 'file';
|
uploadInput.type = 'file';
|
||||||
uploadInput.style.display = 'none';
|
uploadInput.style.display = 'none';
|
||||||
|
|
@ -407,41 +519,16 @@
|
||||||
});
|
});
|
||||||
container.appendChild(uploadInput);
|
container.appendChild(uploadInput);
|
||||||
|
|
||||||
const uploadBtn = document.createElement('button');
|
// Hamburger menu button
|
||||||
uploadBtn.className = 'files-action-btn';
|
const menuBtn = document.createElement('button');
|
||||||
uploadBtn.textContent = '⬆';
|
menuBtn.className = 'files-action-btn files-menu-btn';
|
||||||
uploadBtn.title = 'Upload';
|
menuBtn.innerHTML = '☰';
|
||||||
uploadBtn.addEventListener('click', uploadFile);
|
menuBtn.title = 'Menu';
|
||||||
btnGroup.appendChild(uploadBtn);
|
menuBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
// Delete selected
|
showToolbarMenu(menuBtn);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
btnGroup.appendChild(hiddenBtn);
|
btnGroup.appendChild(menuBtn);
|
||||||
|
|
||||||
// Refresh
|
|
||||||
const refreshBtn = document.createElement('button');
|
|
||||||
refreshBtn.className = 'files-action-btn';
|
|
||||||
refreshBtn.textContent = '↻';
|
|
||||||
refreshBtn.title = 'Refresh';
|
|
||||||
refreshBtn.addEventListener('click', () => loadDirectory(currentPath));
|
|
||||||
btnGroup.appendChild(refreshBtn);
|
|
||||||
|
|
||||||
toolbar.appendChild(btnGroup);
|
toolbar.appendChild(btnGroup);
|
||||||
container.appendChild(toolbar);
|
container.appendChild(toolbar);
|
||||||
|
|
@ -508,6 +595,7 @@
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
hideContextMenu();
|
hideContextMenu();
|
||||||
|
hideToolbarMenu();
|
||||||
container = null;
|
container = null;
|
||||||
fileListEl = null;
|
fileListEl = null;
|
||||||
breadcrumbEl = null;
|
breadcrumbEl = null;
|
||||||
|
|
@ -515,6 +603,7 @@
|
||||||
uploadInput = null;
|
uploadInput = null;
|
||||||
currentPath = '/';
|
currentPath = '/';
|
||||||
selectedFiles.clear();
|
selectedFiles.clear();
|
||||||
|
selectionMode = false;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue