const Browser = (() => {
let currentDirId = null; // null = root
let dirStack = []; // breadcrumb stack: [{id, name}]
let allDirs = []; // flat list for move dialog
async function init() {
document.getElementById('new-dir-btn').addEventListener('click', createDirPrompt);
document.getElementById('upload-btn').addEventListener('click', () => {
document.getElementById('file-input').click();
});
document.getElementById('file-input').addEventListener('change', handleFileUpload);
// Drop zone on map
setupDropZone();
await loadDir(null);
}
async function loadDir(dirId) {
currentDirId = dirId;
renderBreadcrumb();
try {
let dirs, tracks;
if (dirId === null) {
dirs = await API.getDirs();
const allTracks = await API.getTracks('');
tracks = allTracks;
} else {
const data = await API.getDir(dirId);
dirs = data.children || [];
tracks = data.tracks || [];
}
renderList(dirs, tracks);
} catch (e) {
showToast('Error loading directory: ' + e.message, 'error');
renderList([], []);
}
}
function renderBreadcrumb() {
const bc = document.getElementById('breadcrumb');
let html = 'Root';
dirStack.forEach((item) => {
html += ' βΊ ';
html += `${escHtml(item.name)}`;
});
bc.innerHTML = html;
bc.querySelectorAll('.bc-item').forEach(el => {
el.addEventListener('click', () => {
const id = el.dataset.id === 'null' ? null : parseInt(el.dataset.id);
if (id === null) {
dirStack = [];
} else {
const idx = dirStack.findIndex(d => d.id === id);
if (idx >= 0) dirStack = dirStack.slice(0, idx + 1);
}
loadDir(id);
});
});
}
function renderList(dirs, tracks) {
const list = document.getElementById('browser-list');
let html = '';
if (dirs.length === 0 && tracks.length === 0) {
html = '
No files here. Upload a GPX or create a folder.
';
}
for (const dir of dirs) {
html += `
π
${escHtml(dir.name)}
${formatDate(dir.updatedAt)}
`;
}
for (const track of tracks) {
const dist = track.totalDistance ? formatDistance(track.totalDistance) : '';
html += `
πΊοΈ
${escHtml(track.name)}
${dist}
${formatDate(track.trackDate || track.uploadDate)}
`;
}
list.innerHTML = html;
// Bind directory click (navigate into)
list.querySelectorAll('.dir-item').forEach(el => {
el.addEventListener('click', (e) => {
if (e.target.closest('.item-actions')) return;
const id = parseInt(el.dataset.id);
const name = el.querySelector('.item-name').textContent;
dirStack.push({ id, name });
loadDir(id);
});
});
list.querySelectorAll('.rename-dir-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
renameDir(parseInt(btn.dataset.id), btn.dataset.name);
});
});
list.querySelectorAll('.delete-dir-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
confirmDelete('directory', btn.dataset.name, () => deleteDir(parseInt(btn.dataset.id)));
});
});
// Bind track click (open on map)
list.querySelectorAll('.track-item').forEach(el => {
el.addEventListener('click', (e) => {
if (e.target.closest('.item-actions')) return;
openTrack(parseInt(el.dataset.id));
});
});
list.querySelectorAll('.view-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
openTrack(parseInt(btn.dataset.id));
});
});
list.querySelectorAll('.share-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
shareTrack(parseInt(btn.dataset.id));
});
});
list.querySelectorAll('.move-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
showMoveDialog(parseInt(btn.dataset.id), btn.dataset.name);
});
});
list.querySelectorAll('.delete-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
confirmDelete('track', btn.dataset.name, () => deleteTrack(parseInt(btn.dataset.id)));
});
});
}
async function openTrack(trackId) {
try {
if (MapView.hasTrack(trackId)) {
// Toggle off
MapView.removeTrack(trackId);
MapView.setCurrentTrack(null);
document.getElementById('track-info-panel').style.display = 'none';
return;
}
const data = await API.getTrackPoints(trackId);
MapView.addTrack(data, trackId);
MapView.fitTrack(trackId);
MapView.setCurrentTrack(trackId);
showTrackInfo(data.meta);
} catch (e) {
showToast('Error loading track: ' + e.message, 'error');
}
}
function showTrackInfo(meta) {
if (!meta) return;
const panel = document.getElementById('track-info-panel');
document.getElementById('track-info-content').innerHTML = `
${escHtml(meta.name || 'Track')}
${formatDistance(meta.totalDistance)}
${meta.pointCount || 0}
${meta.trackDate ? `${formatDate(meta.trackDate)}
` : ''}
`;
panel.style.display = 'block';
document.getElementById('track-info-close').onclick = () => { panel.style.display = 'none'; };
}
async function createDirPrompt() {
const name = prompt('Folder name:');
if (!name || !name.trim()) return;
try {
await API.createDir(name.trim(), currentDirId);
await loadDir(currentDirId);
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
async function renameDir(id, currentName) {
const name = prompt('New name:', currentName);
if (!name || !name.trim() || name.trim() === currentName) return;
try {
await API.renameDir(id, name.trim());
await loadDir(currentDirId);
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
async function deleteDir(id) {
try {
await API.deleteDir(id);
await loadDir(currentDirId);
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
async function deleteTrack(id) {
try {
MapView.removeTrack(id);
await API.deleteTrack(id);
await loadDir(currentDirId);
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
async function handleFileUpload(e) {
const files = Array.from(e.target.files);
if (files.length === 0) return;
e.target.value = '';
for (const file of files) {
showUploadToast(`Uploading ${file.name}...`);
try {
await API.uploadTrack(file, currentDirId, null);
showToast(`Uploaded: ${file.name}`, 'success');
} catch (err) {
showToast(`Error uploading ${file.name}: ${err.message}`, 'error');
}
}
hideUploadToast();
await loadDir(currentDirId);
}
function setupDropZone() {
const mapContainer = document.getElementById('map-container');
mapContainer.addEventListener('dragover', (e) => {
e.preventDefault();
mapContainer.classList.add('drag-over');
});
mapContainer.addEventListener('dragleave', (e) => {
if (!mapContainer.contains(e.relatedTarget)) {
mapContainer.classList.remove('drag-over');
}
});
mapContainer.addEventListener('drop', async (e) => {
e.preventDefault();
mapContainer.classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files).filter(f => f.name.toLowerCase().endsWith('.gpx'));
if (files.length === 0) {
showToast('No GPX files found in drop', 'error');
return;
}
for (const file of files) {
showUploadToast(`Uploading ${file.name}...`);
try {
await API.uploadTrack(file, currentDirId, null);
showToast(`Uploaded: ${file.name}`, 'success');
} catch (err) {
showToast(`Error: ${err.message}`, 'error');
}
}
hideUploadToast();
await loadDir(currentDirId);
});
}
async function shareTrack(trackId) {
try {
const track = await API.getTrack(trackId);
const modal = document.getElementById('share-modal');
const content = document.getElementById('share-content');
const existingCode = track.ShareLink?.code || track.shareCode;
if (existingCode) {
const shareUrl = buildShareUrl(existingCode);
content.innerHTML = `
Share link:
`;
content.querySelector('#copy-share-btn').addEventListener('click', () => {
navigator.clipboard.writeText(shareUrl).then(() => showToast('Copied!', 'success')).catch(() => {
document.getElementById('share-url-input').select();
document.execCommand('copy');
showToast('Copied!', 'success');
});
});
content.querySelector('#share-revoke-btn').addEventListener('click', async () => {
try {
await API.deleteShare(trackId);
modal.style.display = 'none';
showToast('Share link revoked', 'success');
} catch (err) {
showToast('Error: ' + err.message, 'error');
}
});
} else {
content.innerHTML = `
No share link yet.
`;
content.querySelector('#share-create-btn').addEventListener('click', async () => {
try {
const res = await API.createShare(trackId);
const shareUrl = buildShareUrl(res.code);
content.innerHTML = `
Share link created:
`;
content.querySelector('#copy-share-new-btn').addEventListener('click', () => {
navigator.clipboard.writeText(shareUrl).then(() => showToast('Copied!', 'success')).catch(() => {
document.getElementById('share-url-input-new').select();
document.execCommand('copy');
showToast('Copied!', 'success');
});
});
} catch (err) {
showToast('Error: ' + err.message, 'error');
}
});
}
modal.style.display = 'flex';
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
function buildShareUrl(code) {
const base = window.location.origin + window.location.pathname;
// Ensure we link to share/CODE relative to the app root
const appRoot = base.replace(/\/[^/]*$/, '/');
return appRoot + 'share/' + code;
}
async function showMoveDialog(trackId, trackName) {
try {
allDirs = await loadAllDirs();
const list = document.getElementById('move-dir-list');
let html = 'π (Root)
';
for (const dir of allDirs) {
html += `π ${escHtml(dir.path)}
`;
}
list.innerHTML = html;
let selectedDirId = null;
list.querySelectorAll('.move-dir-item').forEach(el => {
el.addEventListener('click', () => {
list.querySelectorAll('.move-dir-item').forEach(e => e.classList.remove('selected'));
el.classList.add('selected');
selectedDirId = el.dataset.id || null;
});
});
document.getElementById('move-confirm-btn').onclick = async () => {
try {
await API.updateTrack(trackId, { directoryId: selectedDirId || null });
document.getElementById('move-modal').style.display = 'none';
await loadDir(currentDirId);
showToast('Track moved', 'success');
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
};
document.getElementById('move-modal').style.display = 'flex';
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
async function loadAllDirs(parentId, prefix) {
const result = [];
let dirs;
try {
if (parentId !== undefined && parentId !== null) {
const data = await API.getDir(parentId);
dirs = data.children || [];
} else {
dirs = await API.getDirs();
}
} catch (e) {
return result;
}
for (const d of dirs) {
const path = (prefix ? prefix + ' / ' : '') + d.name;
result.push({ id: d.id, name: d.name, path });
const sub = await loadAllDirs(d.id, path);
result.push(...sub);
}
return result;
}
function confirmDelete(type, name, onConfirm) {
document.getElementById('confirm-title').textContent = `Delete ${type}`;
document.getElementById('confirm-message').textContent = `Are you sure you want to delete "${name}"? This cannot be undone.`;
const modal = document.getElementById('confirm-modal');
modal.style.display = 'flex';
document.getElementById('confirm-ok-btn').onclick = () => {
modal.style.display = 'none';
onConfirm();
};
}
function getCurrentDirId() { return currentDirId; }
function refresh() { return loadDir(currentDirId); }
return { init, loadDir, getCurrentDirId, refresh };
})();