import { GetVersion, ListDir, GetConnections, SaveConnection, DeleteConnection, Delete, Rename, PromptUploadFiles, PromptUploadDirectory, PromptDownload, TransferItems, GetTransfers, ClearTransfers, ReorderConnections } from '../wailsjs/go/main/App.js';
let currentConn = 'local';
let currentPath = '';
let connectionsCache = [];
let selectedItems = [];
let lastSelectedPath = null;
let showHiddenFiles = false;
let sortField = 'name';
let sortAsc = true;
let draggedConnId = null;
function uuidv4() {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
async function init() {
if (window.runtime && window.runtime.Environment) {
const env = await window.runtime.Environment();
if (env.platform === 'windows') {
const nfsOpt = document.querySelector('#conn-protocol option[value="nfs"]');
if (nfsOpt) nfsOpt.remove();
}
}
try {
const version = await GetVersion();
const vSpan = document.getElementById('app-version');
if (vSpan && version) {
vSpan.innerText = version;
}
} catch (e) { }
await loadConnections();
await loadDirectory(currentConn, currentPath);
document.getElementById('conn-form').addEventListener('submit', async (e) => {
e.preventDefault();
const conn = {
id: document.getElementById('conn-id').value,
name: document.getElementById('conn-name').value,
protocol: document.getElementById('conn-protocol').value,
host: document.getElementById('conn-host').value,
port: parseInt(document.getElementById('conn-port').value) || 0,
bucket: document.getElementById('conn-bucket').value,
region: document.getElementById('conn-region').value,
path_style: document.getElementById('conn-pathstyle').checked,
secure: document.getElementById('conn-secure').checked,
username: document.getElementById('conn-username').value
};
let secret = document.getElementById('conn-secret').value;
if (conn.protocol === 'sftp' && document.getElementById('conn-sftp-auth-type').value === 'key') {
secret = document.getElementById('conn-secret-key').value;
}
try {
await SaveConnection(conn, secret);
closeConnModal();
loadConnections();
} catch (err) {
alert("Failed to save connection: " + err);
}
});
}
async function loadConnections() {
const list = document.getElementById('conn-list');
list.innerHTML = `
OS Local Filesystem
`;
try {
const conns = await GetConnections();
connectionsCache = conns || [];
if (connectionsCache) {
connectionsCache.forEach(c => {
const el = document.createElement('div');
el.className = `conn-item ${currentConn === c.id ? 'active' : ''}`;
el.innerHTML = `${c.protocol} ${c.name} `;
el.onclick = () => switchConn(c.id);
el.draggable = true;
el.addEventListener('dragstart', (e) => {
draggedConnId = c.id;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', c.id);
el.style.opacity = '0.5';
});
el.addEventListener('dragend', () => {
draggedConnId = null;
el.style.opacity = '1';
});
el.addEventListener('dragenter', (e) => {
e.preventDefault();
if (draggedConnId && draggedConnId !== c.id) {
el.style.boxShadow = 'inset 0 2px 0 var(--accent)';
}
});
el.addEventListener('dragleave', (e) => {
el.style.boxShadow = '';
});
el.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
el.addEventListener('drop', async (e) => {
e.preventDefault();
el.style.boxShadow = '';
if (!draggedConnId || draggedConnId === c.id) return;
const ids = connectionsCache.map(conn => conn.id);
const draggedIdx = ids.indexOf(draggedConnId);
const targetIdx = ids.indexOf(c.id);
if (draggedIdx !== -1 && targetIdx !== -1) {
ids.splice(draggedIdx, 1);
ids.splice(targetIdx, 0, draggedConnId);
try {
await ReorderConnections(ids);
} catch (err) {
console.error("Failed to reorder connections:", err);
}
await loadConnections();
}
});
list.appendChild(el);
});
}
} catch (e) {
console.error(e);
}
}
window.switchConn = async (id) => {
currentConn = id;
currentPath = ''; // Root path
loadConnections(); // To update 'active' class
await loadDirectory(currentConn, currentPath);
};
window.editConnection = (id) => {
const c = connectionsCache.find(conn => conn.id === id);
if (!c) return;
document.getElementById('modal-title').innerText = "Edit Connection";
document.getElementById('conn-id').value = c.id;
document.getElementById('conn-name').value = c.name;
document.getElementById('conn-protocol').value = c.protocol;
document.getElementById('conn-host').value = c.host || '';
document.getElementById('conn-port').value = c.port || '';
document.getElementById('conn-bucket').value = c.bucket || '';
document.getElementById('conn-region').value = c.region || '';
document.getElementById('conn-pathstyle').checked = c.path_style || false;
document.getElementById('conn-secure').checked = c.secure || false;
document.getElementById('conn-username').value = c.username || '';
document.getElementById('conn-secret').value = '';
document.getElementById('conn-secret-key').value = '';
document.getElementById('conn-sftp-auth-type').value = 'password';
document.getElementById('conn-delete-btn').style.display = 'block';
updateProtocolFields();
document.getElementById('conn-modal').style.display = 'flex';
};
window.openConnModal = () => {
document.getElementById('modal-title').innerText = "Add Connection";
document.getElementById('conn-form').reset();
document.getElementById('conn-id').value = uuidv4();
document.getElementById('conn-secure').checked = false;
document.getElementById('conn-secret').value = '';
document.getElementById('conn-secret-key').value = '';
document.getElementById('conn-sftp-auth-type').value = 'password';
document.getElementById('conn-delete-btn').style.display = 'none';
updateProtocolFields();
document.getElementById('conn-modal').style.display = 'flex';
};
window.closeConnModal = () => {
document.getElementById('conn-modal').style.display = 'none';
};
window.updateProtocolFields = () => {
const protocol = document.getElementById('conn-protocol').value;
const showBucket = protocol === 's3' || protocol === 'smb' || protocol === 'nfs';
document.getElementById('bucket-fields').style.display = showBucket ? 'block' : 'none';
const bucketLabel = document.getElementById('bucket-label');
if (bucketLabel) {
if (protocol === 'nfs') {
bucketLabel.innerText = 'Export Path';
} else if (protocol === 'smb') {
bucketLabel.innerText = 'Share';
} else {
bucketLabel.innerText = 'Bucket';
}
}
const isNFS = protocol === 'nfs';
const portGroup = document.getElementById('port-group');
if (portGroup) portGroup.style.display = isNFS ? 'none' : 'block';
const authRow = document.getElementById('auth-row');
if (authRow) authRow.style.display = isNFS ? 'none' : 'flex';
// Only show S3 specific fields when S3 is selected
const showS3Specific = protocol === 's3';
document.getElementById('region-group').style.display = showS3Specific ? 'flex' : 'none';
document.getElementById('pathstyle-group').style.display = showS3Specific ? 'flex' : 'none';
// FTP specific fields
const showFTP = protocol === 'ftp';
document.getElementById('ftp-secure-group').style.display = showFTP ? 'flex' : 'none';
// SFTP Auth Type toggle
const authTypeSelect = document.getElementById('conn-sftp-auth-type');
const secretInput = document.getElementById('conn-secret');
const secretKeyArea = document.getElementById('conn-secret-key');
const secretLabel = document.getElementById('secret-label');
if (protocol === 'sftp') {
authTypeSelect.style.display = 'block';
if (authTypeSelect.value === 'key') {
secretInput.style.display = 'none';
secretKeyArea.style.display = 'block';
secretLabel.innerText = 'SSH Private Key';
} else {
secretInput.style.display = 'block';
secretKeyArea.style.display = 'none';
secretLabel.innerText = 'Password';
}
} else {
authTypeSelect.style.display = 'none';
secretInput.style.display = 'block';
secretKeyArea.style.display = 'none';
secretLabel.innerText = 'Password / Secret Key';
}
};
window.deleteConnection = async () => {
if (!confirm("Are you sure you want to delete this connection?")) return;
const id = document.getElementById('conn-id').value;
try {
await DeleteConnection(id);
closeConnModal();
if (currentConn === id) {
switchConn('local');
} else {
loadConnections();
}
} catch (err) {
alert("Failed to delete connection: " + err);
}
};
async function loadDirectory(connId, path) {
const tbody = document.getElementById('file-list');
tbody.innerHTML = '| Loading... |
';
document.getElementById('breadcrumb').innerText = path || '/';
try {
const files = await ListDir(connId, path);
tbody.innerHTML = '';
selectedItems = [];
lastSelectedPath = null;
let displayFiles = files || [];
if (!showHiddenFiles) {
displayFiles = displayFiles.filter(f => !f.name.startsWith('.'));
}
if (displayFiles.length === 0) {
tbody.innerHTML = '| Empty directory |
';
return;
}
displayFiles.sort((a, b) => {
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
let cmp = 0;
switch (sortField) {
case 'name':
cmp = a.name.localeCompare(b.name);
break;
case 'size':
cmp = a.size - b.size;
break;
case 'modified':
cmp = new Date(a.modified).getTime() - new Date(b.modified).getTime();
if (isNaN(cmp)) cmp = 0;
break;
case 'permissions':
cmp = a.permissions.localeCompare(b.permissions);
break;
}
return sortAsc ? cmp : -cmp;
});
displayFiles.forEach(f => {
const tr = document.createElement('tr');
tr.dataset.path = f.path;
tr.dataset.name = f.name;
tr.dataset.isDir = f.is_dir;
tr.dataset.size = f.size;
tr.innerHTML = `
${f.is_dir ? '📁 ' : '📄 '}${f.name} |
${f.is_dir ? '-' : formatBytes(f.size)} |
${formatDate(f.modified)} |
${f.permissions} |
`;
tr.onclick = (e) => {
const path = f.path;
const isSelected = selectedItems.some(i => i.path === path);
if (e.ctrlKey || e.metaKey) {
if (isSelected) {
selectedItems = selectedItems.filter(i => i.path !== path);
tr.classList.remove('selected');
} else {
selectedItems.push(f);
tr.classList.add('selected');
}
lastSelectedPath = path;
} else if (e.shiftKey && lastSelectedPath) {
const allRows = Array.from(tbody.querySelectorAll('tr'));
const lastIdx = allRows.findIndex(r => r.dataset.path === lastSelectedPath);
const currIdx = allRows.findIndex(r => r.dataset.path === path);
const startIdx = Math.min(lastIdx, currIdx);
const endIdx = Math.max(lastIdx, currIdx);
selectedItems = [];
allRows.forEach(r => r.classList.remove('selected'));
for (let i = startIdx; i <= endIdx; i++) {
const row = allRows[i];
if (row.dataset.path) {
row.classList.add('selected');
selectedItems.push({
path: row.dataset.path,
name: row.dataset.name,
is_dir: row.dataset.isDir === 'true',
size: parseInt(row.dataset.size) || 0
});
}
}
} else {
selectedItems = [f];
Array.from(tbody.querySelectorAll('tr')).forEach(r => r.classList.remove('selected'));
tr.classList.add('selected');
lastSelectedPath = path;
}
};
if (f.is_dir) {
tr.ondblclick = () => {
currentPath = f.path;
loadDirectory(currentConn, currentPath);
};
}
tbody.appendChild(tr);
});
} catch (e) {
tbody.innerHTML = `| Error: ${e} |
`;
}
}
window.refreshCurrentDir = () => {
loadDirectory(currentConn, currentPath);
};
window.showTransfers = () => {
document.getElementById('transfer-tray').style.display = 'block';
};
window.hideTransfers = () => {
document.getElementById('transfer-tray').style.display = 'none';
};
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function formatDate(dateString) {
if (!dateString) return '-';
const d = new Date(dateString);
if (isNaN(d.getTime())) return dateString;
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
window.setSort = (field) => {
if (sortField === field) {
sortAsc = !sortAsc;
} else {
sortField = field;
sortAsc = true;
}
['name', 'size', 'modified', 'permissions'].forEach(f => {
document.getElementById(`sort-ind-${f}`).innerText = '';
});
document.getElementById(`sort-ind-${field}`).innerText = sortAsc ? '↑' : '↓';
refreshCurrentDir();
};
window.clearTransfers = async () => {
try {
await ClearTransfers();
pollTransfers();
} catch (e) {
console.error(e);
}
};
async function pollTransfers() {
try {
const transfers = await GetTransfers();
const list = document.getElementById('transfer-list');
const btn = document.getElementById('transfers-btn');
btn.innerText = `Transfers (${transfers ? transfers.length : 0})`;
if (!transfers || transfers.length === 0) {
list.innerHTML = 'No active transfers
';
return;
}
let html = '';
transfers.forEach(t => {
const pct = t.bytes_total > 0 ? (t.bytes_done / t.bytes_total) * 100 : 0;
const speed = t.status === 'active' ? `${t.speed_mbps.toFixed(2)} MB/s` : t.status;
const eta = t.status === 'active' ? `${t.eta_seconds}s remaining` : '';
const color = t.status === 'failed' ? 'var(--error)' : (t.status === 'complete' ? 'var(--success)' : 'var(--accent)');
html += `
`;
});
list.innerHTML = html;
} catch (e) {
console.error(e);
}
}
setInterval(pollTransfers, 1000);
window.addEventListener('load', init);
// Context Menu Logic
document.addEventListener('contextmenu', (e) => {
const menu = document.getElementById('context-menu');
menu.style.display = 'none';
document.getElementById('ctx-hidden-text').innerText = showHiddenFiles ? 'Hide Hidden Files' : 'Show Hidden Files';
const row = e.target.closest('tr');
const isBrowserPane = e.target.closest('#browser-pane');
if (row && row.dataset.path && !row.closest('#transfer-dest-list')) {
e.preventDefault();
if (!selectedItems.some(i => i.path === row.dataset.path)) {
selectedItems = [{
path: row.dataset.path,
name: row.dataset.name,
is_dir: row.dataset.isDir === 'true',
size: parseInt(row.dataset.size) || 0
}];
const tbody = document.getElementById('file-list');
Array.from(tbody.querySelectorAll('tr')).forEach(r => r.classList.remove('selected'));
row.classList.add('selected');
lastSelectedPath = row.dataset.path;
}
document.getElementById('ctx-rename').style.display = 'block';
document.getElementById('ctx-transfer').style.display = 'block';
document.getElementById('ctx-download').style.display = 'block';
document.getElementById('ctx-delete').style.display = 'block';
document.getElementById('ctx-divider').style.display = 'block';
menu.style.display = 'block';
let left = e.pageX;
let top = e.pageY;
if (left + menu.offsetWidth > window.innerWidth) {
left = window.innerWidth - menu.offsetWidth;
}
if (top + menu.offsetHeight > window.innerHeight) {
top = window.innerHeight - menu.offsetHeight;
}
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
} else if (isBrowserPane && !e.target.closest('#transfer-dest-list')) {
e.preventDefault();
document.getElementById('ctx-rename').style.display = 'none';
document.getElementById('ctx-transfer').style.display = 'none';
document.getElementById('ctx-download').style.display = 'none';
document.getElementById('ctx-delete').style.display = 'none';
document.getElementById('ctx-divider').style.display = 'none';
menu.style.display = 'block';
let left = e.pageX;
let top = e.pageY;
if (left + menu.offsetWidth > window.innerWidth) {
left = window.innerWidth - menu.offsetWidth;
}
if (top + menu.offsetHeight > window.innerHeight) {
top = window.innerHeight - menu.offsetHeight;
}
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
}
});
document.addEventListener('click', () => {
document.getElementById('context-menu').style.display = 'none';
const uploadMenu = document.getElementById('upload-menu');
if (uploadMenu) uploadMenu.style.display = 'none';
});
window.toggleUploadMenu = (e) => {
if (e) e.stopPropagation();
const menu = document.getElementById('upload-menu');
if (menu.style.display === 'none') {
menu.style.display = 'block';
} else {
menu.style.display = 'none';
}
};
window.handleContextMenu = async (action) => {
if (action === 'toggle_hidden') {
showHiddenFiles = !showHiddenFiles;
refreshCurrentDir();
return;
}
if (selectedItems.length === 0) return;
try {
if (action === 'delete') {
if (confirm(`Are you sure you want to delete ${selectedItems.length} items?`)) {
for (const item of selectedItems) {
await Delete(currentConn, item.path);
}
refreshCurrentDir();
}
} else if (action === 'rename') {
if (selectedItems.length > 1) {
alert("Cannot rename multiple items at once.");
return;
}
const item = selectedItems[0];
const newName = prompt(`Enter new name for ${item.name}:`, item.name);
if (newName && newName !== item.name) {
const pathParts = item.path.split('/');
pathParts[pathParts.length - 1] = newName;
const newPath = pathParts.join('/');
await Rename(currentConn, item.path, newPath);
refreshCurrentDir();
}
} else if (action === 'download') {
for (const item of selectedItems) {
await PromptDownload(currentConn, item.path);
}
showTransfers();
} else if (action === 'transfer') {
openTransferModal();
}
} catch (e) {
alert(`Failed to ${action}: ${e}`);
}
};
window.navigateUp = () => {
if (!currentPath || currentPath === '/') return;
const parts = currentPath.split('/').filter(p => p);
parts.pop();
currentPath = parts.length ? '/' + parts.join('/') : '';
loadDirectory(currentConn, currentPath);
};
let transferDestConn = 'local';
let transferDestPath = '';
window.openTransferModal = () => {
const select = document.getElementById('transfer-dest-conn');
select.innerHTML = ``;
connectionsCache.forEach(c => {
select.innerHTML += ``;
});
document.getElementById('transfer-modal').style.display = 'flex';
loadTransferDestRoot();
};
window.closeTransferModal = () => {
document.getElementById('transfer-modal').style.display = 'none';
document.getElementById('transfer-verify').checked = false;
document.getElementById('transfer-limit').value = "0";
};
window.loadTransferDestRoot = () => {
transferDestConn = document.getElementById('transfer-dest-conn').value;
transferDestPath = '';
loadTransferDestDirectory(transferDestConn, transferDestPath);
};
window.loadTransferDestDirectory = async (connId, path) => {
transferDestPath = path;
const tbody = document.getElementById('transfer-dest-list');
tbody.innerHTML = '| Loading... |
';
document.getElementById('transfer-dest-breadcrumb').innerText = path || '/';
try {
const files = await ListDir(connId, path);
tbody.innerHTML = '';
if (path && path !== '/') {
const tr = document.createElement('tr');
tr.innerHTML = `📁 .. | `;
tr.style.cursor = 'pointer';
tr.ondblclick = () => {
const parts = path.split('/').filter(p => p);
parts.pop();
loadTransferDestDirectory(connId, parts.length ? '/' + parts.join('/') : '');
};
tbody.appendChild(tr);
}
if (!files || files.length === 0) {
tbody.innerHTML += '| Empty directory |
';
return;
}
files.forEach(f => {
const tr = document.createElement('tr');
tr.style.cursor = 'pointer';
tr.innerHTML = `${f.is_dir ? '📁 ' : '📄 '}${f.name} | `;
if (f.is_dir) {
tr.ondblclick = () => {
loadTransferDestDirectory(connId, f.path);
};
}
tbody.appendChild(tr);
});
} catch (e) {
tbody.innerHTML = `| Error: ${e} |
`;
}
};
window.executeTransfer = async () => {
if (selectedItems.length === 0) return;
try {
const verify = document.getElementById('transfer-verify').checked;
const limit = parseInt(document.getElementById('transfer-limit').value, 10) || 0;
await TransferItems(currentConn, transferDestConn, transferDestPath, selectedItems, verify, limit);
closeTransferModal();
showTransfers();
selectedItems = [];
Array.from(document.querySelectorAll('#file-list tr')).forEach(r => r.classList.remove('selected'));
} catch (e) {
alert("Transfer failed: " + e);
}
};
window.promptUploadFiles = async () => {
try {
await PromptUploadFiles(currentConn, currentPath);
showTransfers();
} catch (e) {
alert("Upload failed: " + e);
}
};
window.promptUploadDirectory = async () => {
try {
await PromptUploadDirectory(currentConn, currentPath);
showTransfers();
} catch (e) {
alert("Upload failed: " + e);
}
};