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;
},
});
})();