From 3c295ba302fb3d64165f05e8d058d00af23673d5 Mon Sep 17 00:00:00 2001 From: roberts Date: Sun, 15 Mar 2026 00:48:33 -0500 Subject: [PATCH] Replace file manager toolbar buttons with hamburger menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/css/apps/files.css | 29 ++++- frontend/js/apps/files.js | 219 +++++++++++++++++++++++++----------- 2 files changed, 180 insertions(+), 68 deletions(-) diff --git a/frontend/css/apps/files.css b/frontend/css/apps/files.css index a3674c2..768a024 100644 --- a/frontend/css/apps/files.css +++ b/frontend/css/apps/files.css @@ -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; +} diff --git a/frontend/js/apps/files.js b/frontend/js/apps/files.js index 307fea7..c4ceef2 100644 --- a/frontend/js/apps/files.js +++ b/frontend/js/apps/files.js @@ -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 = `
Error: ${e.message}
`; } @@ -108,38 +110,61 @@ row.dataset.path = entry.path; const icon = getFileIcon(entry); - row.innerHTML = ` -
- ${icon} - ${entry.name} -
- ${entry.is_dir ? '--' : formatSize(entry.size)} - ${formatDate(entry.modified)} - ${entry.permissions} - `; - // 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 = ` +
+ ${checked ? '☑' : '☐'} + ${icon} + ${entry.name} +
+ ${entry.is_dir ? '--' : formatSize(entry.size)} + ${formatDate(entry.modified)} + ${entry.permissions} + `; + row.addEventListener('click', () => { if (selectedFiles.has(entry.path)) { selectedFiles.delete(entry.path); } else { selectedFiles.add(entry.path); } - } - }); + renderFileList(lastEntries); + }); + } else { + row.innerHTML = ` +
+ ${icon} + ${entry.name} +
+ ${entry.is_dir ? '--' : formatSize(entry.size)} + ${formatDate(entry.modified)} + ${entry.permissions} + `; - // 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 = '.'; - 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; }, }); })();