(function(){
const FileManager = {
id: "fileManager",
label: "📁 File Manager",
html: `
<div class="fm-container">
<div class="fm-toolbar">
<div class="fm-toolbar-left" data-toolbar></div>
<span id="fmStatus" class="fm-status">Not connected</span>
</div>
<div class="fm-breadcrumb" id="fmBreadcrumb">
<span class="fm-crumb" data-path="/">🏠 Root</span>
</div>
<div id="fmList" class="fm-list">
<div class="fm-empty">
<div class="fm-empty-icon">📂</div>
<div class="fm-empty-text">Connect to a server to browse files</div>
</div>
</div>
</div>
<style>
.fm-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 1rem;
gap: 1rem;
}
.fm-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.fm-toolbar-left {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.fm-btn {
padding: 0.5rem 1rem;
background: #2a3648;
color: #e6edf3;
border: 1px solid #3b4557;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.fm-btn:hover {
background: #334155;
border-color: #3b82f6;
}
.fm-btn:active {
transform: scale(0.98);
}
.fm-status {
font-size: 0.875rem;
color: #94a3b8;
padding: 0.5rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fm-breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem;
background: rgba(30, 41, 59, 0.5);
border-radius: 8px;
font-size: 0.875rem;
overflow-x: auto;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.fm-crumb {
cursor: pointer;
color: #3b82f6;
transition: color 0.15s;
white-space: nowrap;
padding: 0.25rem 0.5rem;
border-radius: 4px;
}
.fm-crumb:hover {
color: #60a5fa;
background: rgba(59, 130, 246, 0.1);
}
.fm-crumb-sep {
color: #64748b;
}
.fm-list {
flex: 1;
overflow-y: auto;
background: rgba(15, 23, 37, 0.5);
border: 1px solid #2a3648;
border-radius: 12px;
padding: 1rem;
}
.fm-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #64748b;
text-align: center;
padding: 2rem;
}
.fm-empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.fm-empty-text {
font-size: 1rem;
}
.fm-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.875rem;
background: rgba(30, 41, 59, 0.3);
border: 1px solid #2a3648;
border-radius: 8px;
margin-bottom: 0.5rem;
cursor: pointer;
transition: all 0.15s;
}
.fm-item:hover {
background: rgba(30, 41, 59, 0.6);
border-color: #3b82f6;
}
.fm-item:active {
transform: scale(0.99);
}
.fm-item-left {
flex: 1;
min-width: 0;
}
.fm-name {
font-weight: 600;
color: #e6edf3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9375rem;
}
.fm-meta {
font-size: 0.8125rem;
color: #94a3b8;
margin-top: 0.25rem;
}
.fm-item-actions {
display: flex;
gap: 0.25rem;
flex-shrink: 0;
}
.fm-icon-btn {
background: transparent;
border: none;
color: #9aa4b2;
cursor: pointer;
padding: 0.5rem;
border-radius: 6px;
font-size: 1.125rem;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
min-width: 2rem;
min-height: 2rem;
}
.fm-icon-btn:hover {
background: #263244;
color: #e6edf3;
}
.fm-icon-btn:active {
transform: scale(0.9);
}
/* Mobile optimizations */
@media (max-width: 640px) {
.fm-container {
padding: 0.75rem;
}
.fm-toolbar {
gap: 0.5rem;
}
.fm-btn {
padding: 0.625rem 0.875rem;
font-size: 0.8125rem;
}
.fm-status {
font-size: 0.75rem;
flex: 1;
text-align: right;
}
.fm-breadcrumb {
padding: 0.5rem;
gap: 0.25rem;
}
.fm-item {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding: 0.75rem;
}
.fm-item-left {
width: 100%;
}
.fm-item-actions {
width: 100%;
justify-content: flex-end;
border-top: 1px solid #2a3648;
padding-top: 0.5rem;
}
.fm-name {
font-size: 0.875rem;
}
.fm-meta {
font-size: 0.75rem;
}
}
/* Loading state */
.fm-loading {
color: #94a3b8;
text-align: center;
padding: 2rem;
}
/* Error state */
.fm-error {
color: #ef4444;
text-align: center;
padding: 2rem;
}
</style>
`,
toolbar: [
{ id: "up", label: "⬅️ Up", action: "goUpDirectory" },
{ id: "refresh", label: "🔄 Refresh", action: "reload" }
],
currentPath: "/",
async onRender(el) {
this.el = el;
this.renderToolbar();
await this.checkConnectionAndLoad();
},
async checkConnectionAndLoad() {
const fmStatus = this.el.querySelector("#fmStatus");
const fmList = this.el.querySelector("#fmList");
try {
const formData = new FormData();
formData.append('sftp_action', 'status');
const res = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success && data.data?.connected) {
fmStatus.textContent = `Connected to ${data.data.config?.host || 'server'}`;
await this.loadFiles("/");
} else {
fmList.innerHTML = `
<div class="fm-empty">
<div class="fm-empty-icon">🔌</div>
<div class="fm-empty-text">Not connected to any SFTP server.<br>Please connect first.</div>
</div>
`;
fmStatus.textContent = "Not connected";
}
} catch (err) {
fmList.innerHTML = `
<div class="fm-error">
Error checking connection: ${err.message}
</div>
`;
fmStatus.textContent = "Connection error";
}
},
renderToolbar() {
const bar = this.el.querySelector("[data-toolbar]");
bar.innerHTML = "";
this.toolbar.forEach(btn => {
const el = document.createElement("button");
el.className = "fm-btn";
el.textContent = btn.label;
el.addEventListener("click", () => this.runAction(btn.action));
bar.appendChild(el);
});
},
runAction(action) {
if (FileManagerActions[action]) FileManagerActions[action](this);
},
async loadFiles(path="/") {
const fmList = this.el.querySelector("#fmList");
const fmStatus = this.el.querySelector("#fmStatus");
fmList.innerHTML = `<div class="fm-loading">Loading ${path}...</div>`;
fmStatus.textContent = `Loading ${path}...`;
this.currentPath = path;
try {
const formData = new FormData();
formData.append('sftp_action', 'list');
formData.append('path', path);
const res = await fetch(window.location.href, {
method: "POST",
body: formData
});
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error("Server returned non-JSON:\n" + text);
}
if (!data.success) throw new Error(data.message);
this.renderFiles(data.data);
this.updateBreadcrumb(path);
fmStatus.textContent = `${data.data.length} items`;
} catch (err) {
fmList.innerHTML = `<div class="fm-error">${err.message}</div>`;
fmStatus.textContent = "Error loading files";
}
},
updateBreadcrumb(path) {
const breadcrumb = this.el.querySelector("#fmBreadcrumb");
const parts = path.split("/").filter(Boolean);
let html = '<span class="fm-crumb" data-path="/">🏠 Root</span>';
let currentPath = '';
parts.forEach((part, index) => {
currentPath += '/' + part;
const pathForClick = currentPath;
html += ` <span class="fm-crumb-sep">/</span> <span class="fm-crumb" data-path="${pathForClick}">${part}</span>`;
});
breadcrumb.innerHTML = html;
// Add click handlers to breadcrumb items
breadcrumb.querySelectorAll('.fm-crumb').forEach(crumb => {
crumb.addEventListener('click', () => {
const targetPath = crumb.getAttribute('data-path');
this.loadFiles(targetPath);
});
});
},
renderFiles(files) {
const fmList = this.el.querySelector("#fmList");
fmList.innerHTML = "";
if (!files || files.length === 0) {
fmList.innerHTML = `
<div class="fm-empty">
<div class="fm-empty-icon">📂</div>
<div class="fm-empty-text">This folder is empty</div>
</div>
`;
return;
}
files.forEach(f => {
const div = document.createElement("div");
div.className = "fm-item";
const isDir = f.type === 'directory' || f.is_dir;
const icon = isDir ? "📁" : this.getFileIcon(f.name);
const fullPath = this.currentPath === '/' ? '/' + f.name : this.currentPath + '/' + f.name;
div.innerHTML = `
<div class="fm-item-left">
<div class="fm-name">${icon} ${this.escapeHtml(f.name)}</div>
<div class="fm-meta">${isDir ? "Folder" : this.formatBytes(f.size)} • ${f.modified || ''}</div>
</div>
${!isDir ? `
<div class="fm-item-actions">
<button class="fm-icon-btn" data-action="download" title="Download">⬇️</button>
<button class="fm-icon-btn" data-action="delete" title="Delete">🗑️</button>
</div>
` : ''}
`;
// Folder click = open next directory
if (isDir) {
div.addEventListener("click", (e) => {
if (!e.target.closest('.fm-icon-btn')) {
this.loadFiles(fullPath);
}
});
} else {
// File actions
const downloadBtn = div.querySelector('[data-action="download"]');
const deleteBtn = div.querySelector('[data-action="delete"]');
if (downloadBtn) {
downloadBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.downloadFile(fullPath, f.name);
});
}
if (deleteBtn) {
deleteBtn.addEventListener('click', async (e) => {
e.stopPropagation();
if (confirm(`Delete ${f.name}?`)) {
await this.deleteFile(fullPath);
}
});
}
}
fmList.appendChild(div);
});
},
async downloadFile(path, filename) {
// This would need to be implemented on the server side
alert('Download functionality needs server implementation');
},
async deleteFile(path) {
try {
const formData = new FormData();
formData.append('sftp_action', 'delete');
formData.append('path', path);
const res = await fetch(window.location.href, {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
await this.loadFiles(this.currentPath);
} else {
alert('Delete failed: ' + data.message);
}
} catch (err) {
alert('Delete error: ' + err.message);
}
},
getFileIcon(filename) {
const ext = filename.split('.').pop().toLowerCase();
const icons = {
'pdf': '📄', 'doc': '📝', 'docx': '📝', 'txt': '📝',
'jpg': '🖼️', 'jpeg': '🖼️', 'png': '🖼️', 'gif': '🖼️', 'svg': '🖼️',
'zip': '📦', 'rar': '📦', 'tar': '📦', 'gz': '📦',
'mp3': '🎵', 'wav': '🎵', 'mp4': '🎬', 'avi': '🎬',
'html': '🌐', 'css': '🎨', 'js': '⚙️', 'json': '📋',
'php': '🐘', 'py': '🐍', 'java': '☕', 'cpp': '⚡'
};
return icons[ext] || '📄';
},
formatBytes(bytes) {
if (!bytes) return "";
if (bytes < 1024) return bytes + " B";
const units = ["KB", "MB", "GB", "TB"];
let u = -1;
do { bytes /= 1024; ++u; } while (bytes >= 1024 && u < units.length - 1);
return bytes.toFixed(1) + " " + units[u];
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
const FileManagerActions = {
reload: ctx => ctx.loadFiles(ctx.currentPath),
goUpDirectory: ctx => {
const parts = ctx.currentPath.split("/").filter(Boolean);
parts.pop();
const newPath = "/" + parts.join("/");
ctx.loadFiles(newPath || "/");
}
};
window.AppItems = window.AppItems || [];
window.AppItems.push(FileManager);
console.log('[filemanager.js] Loaded - using integrated SFTP connection');
})();