/* Atlus — File Manager app */
(function () {
'use strict';
let container = null;
let currentPath = '/';
let fileListEl = null;
let breadcrumbEl = null;
let sidebarEl = null;
let selectedFiles = new Set();
let contextMenuEl = null;
let uploadInput = null;
const FILE_ICONS = {
dir: '📁',
file: '📄',
image: '🖼',
video: '🎬',
audio: '🎵',
archive: '📦',
code: '📝',
text: '📝',
};
function getFileIcon(entry) {
if (entry.is_dir) return FILE_ICONS.dir;
const ext = entry.name.split('.').pop().toLowerCase();
if (['jpg','jpeg','png','gif','bmp','svg','webp','fits','fit'].includes(ext)) return FILE_ICONS.image;
if (['mp4','mkv','avi','mov','webm'].includes(ext)) return FILE_ICONS.video;
if (['mp3','flac','wav','ogg','aac'].includes(ext)) return FILE_ICONS.audio;
if (['zip','tar','gz','bz2','xz','7z','rar'].includes(ext)) return FILE_ICONS.archive;
if (['js','py','sh','css','html','json','yml','yaml','toml','rs','go','c','cpp','h'].includes(ext)) return FILE_ICONS.code;
return FILE_ICONS.file;
}
function formatSize(bytes) {
return Atlus.formatBytes(bytes);
}
function formatDate(ts) {
return new Date(ts * 1000).toLocaleString('en-US', {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false,
});
}
// ---- Open file in editor ----
function openInEditor(entry) {
Atlus.openApp('editor');
// Give editor time to init, then open file
setTimeout(() => {
if (Atlus.apps.editor && Atlus.apps.editor.openFile) {
Atlus.apps.editor.openFile(entry.path, entry.mime);
}
}, 50);
}
// ---- Directory operations ----
async function loadDirectory(path) {
currentPath = path;
selectedFiles.clear();
updateBreadcrumb();
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);
} catch (e) {
fileListEl.innerHTML = `
Error: ${e.message}
`;
}
// Update sidebar active
if (sidebarEl) {
sidebarEl.querySelectorAll('.sidebar-item').forEach(item => {
item.classList.toggle('active', item.dataset.path === path);
});
}
}
function renderFileList(entries) {
fileListEl.innerHTML = '';
// Parent directory entry
if (currentPath !== '/') {
const parentRow = document.createElement('div');
parentRow.className = 'file-row';
parentRow.innerHTML = `
....
`;
parentRow.addEventListener('click', () => {
const parent = currentPath.replace(/\/[^/]+\/?$/, '') || '/';
loadDirectory(parent);
});
fileListEl.appendChild(parentRow);
}
entries.forEach(entry => {
const row = document.createElement('div');
row.className = 'file-row';
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 (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;
row.addEventListener('touchstart', (e) => {
pressTimer = setTimeout(() => showContextMenu(e, entry), 500);
});
row.addEventListener('touchend', () => clearTimeout(pressTimer));
row.addEventListener('touchmove', () => clearTimeout(pressTimer));
// Right-click fallback
row.addEventListener('contextmenu', (e) => {
e.preventDefault();
showContextMenu(e, entry);
});
fileListEl.appendChild(row);
});
}
function updateBreadcrumb() {
breadcrumbEl.innerHTML = '';
const parts = currentPath.split('/').filter(Boolean);
// Root
const root = document.createElement('button');
root.className = 'breadcrumb-segment';
root.textContent = '/';
root.addEventListener('click', () => loadDirectory('/'));
breadcrumbEl.appendChild(root);
let path = '';
parts.forEach((part, i) => {
path += '/' + part;
const sep = document.createElement('span');
sep.className = 'breadcrumb-sep';
sep.textContent = '/';
breadcrumbEl.appendChild(sep);
const seg = document.createElement('button');
seg.className = 'breadcrumb-segment';
seg.textContent = part;
const segPath = path;
seg.addEventListener('click', () => loadDirectory(segPath));
breadcrumbEl.appendChild(seg);
});
}
// ---- Context menu ----
function showContextMenu(e, entry) {
hideContextMenu();
contextMenuEl = document.createElement('div');
contextMenuEl.className = 'file-context-menu';
const items = [];
// If right-clicked on empty area or a specific entry
if (entry) {
if (entry.is_dir) {
items.push({ label: 'Open', action: () => loadDirectory(entry.path) });
} else {
items.push({ label: 'Open in Editor', action: () => openInEditor(entry) });
}
items.push({ label: 'Rename', action: () => renameFile(entry) });
items.push({ label: 'Copy', action: () => { /* TODO */ } });
items.push({ label: 'Move', action: () => { /* TODO */ } });
items.push({ sep: true });
}
items.push({ label: 'New File', action: () => createNewFile() });
items.push({ label: 'New Folder', action: () => createNewFolder() });
if (entry) {
items.push({ sep: true });
items.push({ label: 'Delete', action: () => deleteFile(entry), danger: true });
}
items.forEach(item => {
if (item.sep) {
const sep = document.createElement('div');
sep.className = 'context-sep';
contextMenuEl.appendChild(sep);
return;
}
const btn = document.createElement('button');
btn.className = 'context-item' + (item.danger ? ' danger' : '');
btn.textContent = item.label;
btn.addEventListener('click', () => {
hideContextMenu();
item.action();
});
contextMenuEl.appendChild(btn);
});
// Position
const x = e.touches ? e.touches[0].clientX : e.clientX;
const y = e.touches ? e.touches[0].clientY : e.clientY;
contextMenuEl.style.left = x + 'px';
contextMenuEl.style.top = y + 'px';
document.body.appendChild(contextMenuEl);
// Close on outside click
setTimeout(() => {
document.addEventListener('click', hideContextMenu, { once: true });
}, 10);
}
function hideContextMenu() {
if (contextMenuEl) {
contextMenuEl.remove();
contextMenuEl = null;
}
}
// ---- File operations ----
async function createNewFile() {
const name = prompt('New file name:');
if (!name) return;
const filePath = (currentPath === '/' ? '/' : currentPath + '/') + name;
try {
await Atlus.apiFetch('/api/files/write', {
method: 'POST',
body: { path: filePath, content: '' },
});
loadDirectory(currentPath);
} catch (e) { /* ignore */ }
}
async function createNewFolder() {
const name = prompt('New folder name:');
if (!name) return;
const folderPath = (currentPath === '/' ? '/' : currentPath + '/') + name;
try {
await Atlus.apiFetch('/api/files/mkdir', {
method: 'POST',
body: { path: folderPath },
});
loadDirectory(currentPath);
} catch (e) { /* ignore */ }
}
async function deleteFile(entry) {
if (!confirm(`Delete "${entry.name}"?`)) return;
await Atlus.apiFetch('/api/files/delete', {
method: 'POST',
body: { path: entry.path },
});
loadDirectory(currentPath);
}
async function deleteSelected() {
if (selectedFiles.size === 0) return;
const count = selectedFiles.size;
if (!confirm(`Delete ${count} selected item${count > 1 ? 's' : ''}?`)) return;
for (const path of selectedFiles) {
await Atlus.apiFetch('/api/files/delete', {
method: 'POST',
body: { path },
});
}
selectedFiles.clear();
loadDirectory(currentPath);
}
async function renameFile(entry) {
const newName = prompt('New name:', entry.name);
if (!newName || newName === entry.name) return;
await Atlus.apiFetch('/api/files/rename', {
method: 'POST',
body: { old_path: entry.path, new_name: newName },
});
loadDirectory(currentPath);
}
async function uploadFile() {
if (!uploadInput) return;
uploadInput.click();
}
async function handleUpload(file) {
const formData = new FormData();
formData.append('file', file);
try {
await Atlus.apiFetch(`/api/files/upload?dest_dir=${encodeURIComponent(currentPath)}`, {
method: 'POST',
body: formData,
headers: {}, // Let browser set content-type for multipart
});
loadDirectory(currentPath);
} catch (e) { /* ignore */ }
}
// ---- Mounts ----
async function loadMounts() {
try {
const res = await Atlus.apiFetch('/api/files/mounts');
if (!res.ok) return;
const mounts = await res.json();
// Add mount entries to sidebar
const heading = document.createElement('div');
heading.className = 'sidebar-heading';
heading.textContent = 'MOUNTS';
sidebarEl.appendChild(heading);
mounts.forEach(mount => {
if (mount.mountpoint === '/') return; // Already in sidebar
const item = document.createElement('button');
item.className = 'sidebar-item';
item.dataset.path = mount.mountpoint;
item.innerHTML = `${mount.mountpoint.split('/').pop() || mount.mountpoint}`;
item.addEventListener('click', () => loadDirectory(mount.mountpoint));
sidebarEl.appendChild(item);
});
} catch (e) {}
}
// ---- App registration ----
Atlus.registerApp('files', {
title: 'Files',
init(el) {
container = el;
container.classList.add('app-files');
// Toolbar
const toolbar = document.createElement('div');
toolbar.className = 'files-toolbar';
breadcrumbEl = document.createElement('div');
breadcrumbEl.className = 'files-breadcrumb';
toolbar.appendChild(breadcrumbEl);
// Toolbar buttons
const btnGroup = document.createElement('div');
btnGroup.className = 'files-btn-group';
// New File
const newFileBtn = document.createElement('button');
newFileBtn.className = 'files-action-btn';
newFileBtn.textContent = '📄+';
newFileBtn.title = 'New File';
newFileBtn.addEventListener('click', createNewFile);
btnGroup.appendChild(newFileBtn);
// New Folder
const newFolderBtn = document.createElement('button');
newFolderBtn.className = 'files-action-btn';
newFolderBtn.textContent = '📁+';
newFolderBtn.title = 'New Folder';
newFolderBtn.addEventListener('click', createNewFolder);
btnGroup.appendChild(newFolderBtn);
// Upload
uploadInput = document.createElement('input');
uploadInput.type = 'file';
uploadInput.style.display = 'none';
uploadInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleUpload(e.target.files[0]);
uploadInput.value = '';
}
});
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);
// 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);
container.appendChild(toolbar);
// Body
const body = document.createElement('div');
body.className = 'files-body';
// Sidebar
sidebarEl = document.createElement('div');
sidebarEl.className = 'files-sidebar';
const homeHeading = document.createElement('div');
homeHeading.className = 'sidebar-heading';
homeHeading.textContent = 'PLACES';
sidebarEl.appendChild(homeHeading);
const places = [
{ icon: '🏠', label: 'Home', path: `/home/${Atlus.user}` },
{ icon: '/', label: 'Root', path: '/' },
{ icon: '📁', label: 'tmp', path: '/tmp' },
];
places.forEach(p => {
const item = document.createElement('button');
item.className = 'sidebar-item';
item.dataset.path = p.path;
item.innerHTML = `${p.label}`;
item.addEventListener('click', () => loadDirectory(p.path));
sidebarEl.appendChild(item);
});
body.appendChild(sidebarEl);
// File list panel
const listPanel = document.createElement('div');
listPanel.className = 'files-list-panel';
const header = document.createElement('div');
header.className = 'files-list-header';
header.innerHTML = 'NameSizeModifiedPerms';
listPanel.appendChild(header);
fileListEl = document.createElement('div');
fileListEl.className = 'files-list';
// Right-click on empty area for context menu
fileListEl.addEventListener('contextmenu', (e) => {
if (e.target === fileListEl || e.target.closest('.file-row') === null) {
e.preventDefault();
showContextMenu(e, null);
}
});
listPanel.appendChild(fileListEl);
body.appendChild(listPanel);
container.appendChild(body);
// Load initial directory
loadDirectory(`/home/${Atlus.user}`);
loadMounts();
},
destroy() {
hideContextMenu();
container = null;
fileListEl = null;
breadcrumbEl = null;
sidebarEl = null;
uploadInput = null;
currentPath = '/';
selectedFiles.clear();
},
});
})();