/* 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; let showHidden = localStorage.getItem('atlus_files_show_hidden') === '1'; let selectionMode = false; let lastEntries = []; // cached for re-render without refetch 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'); lastEntries = await res.json(); renderFileList(lastEntries); } 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); } const visible = showHidden ? entries : entries.filter(e => !e.name.startsWith('.')); visible.forEach(entry => { const row = document.createElement('div'); row.className = 'file-row'; row.dataset.path = entry.path; const icon = getFileIcon(entry); 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} `; // 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; 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 (right-click on files) ---- function showContextMenu(e, entry) { hideContextMenu(); contextMenuEl = document.createElement('div'); contextMenuEl.className = 'file-context-menu'; const items = []; // If right-clicked on 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; } } // ---- 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() { 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(); selectionMode = false; 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); // Hidden upload input 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); // 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(menuBtn); 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.icon}${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(); hideToolbarMenu(); container = null; fileListEl = null; breadcrumbEl = null; sidebarEl = null; uploadInput = null; currentPath = '/'; selectedFiles.clear(); selectionMode = false; }, }); })();