const TRACK_TYPE_EMOJI = {
hiking: 'π₯Ύ',
running: 'π',
cycling: 'π΄',
driving: 'π',
train: 'π',
other: 'π',
};
const Browser = (() => {
let selectedDirId = null; // used as upload context
let expandedDirs = new Set();
let dirContents = {}; // 'root' | dirId -> { dirs, tracks }
let dirMeta = {}; // dirId -> { id, name, parentId }
let dragTrackIds = []; // ids being dragged (may be multiple)
let dragDirId = null; // dir id being dragged (single)
let allDirs = []; // flat list for move dialog
// ===== Multi-select state =====
let selectedTrackIds = new Set(); // Ctrl/Shift-selected track ids
let lastClickedTrackId = null; // anchor for Shift-range
// ===== Auto-scroll state =====
let autoScrollRaf = null;
let autoScrollDir = 0; // -1 up, +1 down, 0 stopped
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);
setupDropZone();
await reload();
}
async function reload() {
const prevExpanded = new Set(expandedDirs);
dirContents = {};
dirMeta = {};
selectedTrackIds.clear();
await loadRootContents();
// Re-fetch previously expanded dirs to restore tree state
for (const dirId of prevExpanded) {
try {
const data = await API.getDir(dirId);
dirMeta[data.id] = { id: data.id, name: data.name, parentId: data.parentId };
dirContents[dirId] = { dirs: data.children || [], tracks: data.tracks || [] };
(data.children || []).forEach(d => {
dirMeta[d.id] = { id: d.id, name: d.name, parentId: dirId };
});
} catch (e) {
expandedDirs.delete(dirId);
}
}
renderBreadcrumb();
renderTree();
}
async function loadRootContents() {
const [dirs, tracks] = await Promise.all([
API.getDirs(),
API.getTracks('')
]);
dirContents['root'] = { dirs, tracks };
dirs.forEach(d => { dirMeta[d.id] = { id: d.id, name: d.name, parentId: null }; });
}
// ===== Breadcrumb =====
function renderBreadcrumb() {
const bc = document.getElementById('breadcrumb');
const path = buildPath(selectedDirId);
let html = 'Root';
for (const item of path) {
html += ' βΊ ';
html += `${escHtml(item.name)}`;
}
bc.innerHTML = html;
bc.querySelectorAll('.bc-item').forEach(el => {
el.addEventListener('click', () => {
selectedDirId = el.dataset.id === 'null' ? null : parseInt(el.dataset.id);
renderBreadcrumb();
renderTree();
});
});
}
function buildPath(dirId) {
if (dirId === null) return [];
const result = [];
let current = dirId;
const seen = new Set();
while (current !== null && current !== undefined && !seen.has(current)) {
seen.add(current);
const meta = dirMeta[current];
if (!meta) break;
result.unshift({ id: meta.id, name: meta.name });
current = meta.parentId;
}
return result;
}
// ===== Tree Rendering =====
function renderTree() {
const list = document.getElementById('browser-list');
const root = dirContents['root'];
if (!root) {
list.innerHTML = '
Loading...
';
return;
}
let html = buildTreeHtml(root.dirs, root.tracks, 0);
if (!html.trim()) {
html = 'No files here. Upload a GPX or create a folder.
';
}
list.innerHTML = html;
bindEvents(list);
}
function buildTreeHtml(dirs, tracks, depth) {
let html = '';
const baseIndent = depth * 16;
for (const dir of dirs) {
const isExpanded = expandedDirs.has(dir.id);
const isSelected = selectedDirId === dir.id;
html += `
π
${escHtml(dir.name)}
${formatDate(dir.updatedAt)}
`;
if (isExpanded) {
const cached = dirContents[dir.id];
if (cached) {
html += buildTreeHtml(cached.dirs, cached.tracks, depth + 1);
} else {
html += `Loading...
`;
}
}
}
for (const track of tracks) {
const dist = track.totalDistance ? formatDistance(track.totalDistance) : '';
const isSel = selectedTrackIds.has(track.id);
const typeEmoji = TRACK_TYPE_EMOJI[track.trackType];
const typeTitle = track.trackType
? track.trackType.charAt(0).toUpperCase() + track.trackType.slice(1)
: '';
const emojiPrefix = typeEmoji
? `${typeEmoji} `
: '';
html += `
πΊοΈ
${emojiPrefix}${escHtml(track.name)}
${dist}
${formatDate(track.trackDate || track.uploadDate)}
`;
}
return html;
}
// ===== Event Binding =====
function bindEvents(container) {
// Expand/collapse triangle
container.querySelectorAll('.expand-btn[data-expand]').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
await toggleExpand(parseInt(btn.dataset.expand));
});
});
// Dir click β select + zoom map to folder tracks
container.querySelectorAll('.dir-item').forEach(el => {
el.addEventListener('click', async (e) => {
if (e.target.closest('.item-actions') || e.target.classList.contains('expand-btn')) return;
await selectDir(parseInt(el.dataset.id));
});
});
container.querySelectorAll('.rename-dir-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
renameDir(parseInt(btn.dataset.id), btn.dataset.name);
});
});
container.querySelectorAll('.delete-dir-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
confirmDelete('directory', btn.dataset.name, () => deleteDir(parseInt(btn.dataset.id)));
});
});
// Track click β open on map / multi-select; hover β highlight on map
container.querySelectorAll('.track-item').forEach(el => {
el.addEventListener('click', (e) => {
if (e.target.closest('.item-actions')) return;
const id = parseInt(el.dataset.id);
if (e.ctrlKey || e.metaKey) {
// Ctrl/Cmd: toggle this item in selection
if (selectedTrackIds.has(id)) {
selectedTrackIds.delete(id);
} else {
selectedTrackIds.add(id);
}
lastClickedTrackId = id;
renderTree();
return;
}
if (e.shiftKey && lastClickedTrackId !== null) {
// Shift: range select between lastClickedTrackId and this one
const allItems = Array.from(container.querySelectorAll('.track-item'))
.map(el2 => parseInt(el2.dataset.id));
const a = allItems.indexOf(lastClickedTrackId);
const b = allItems.indexOf(id);
if (a !== -1 && b !== -1) {
const [lo, hi] = a < b ? [a, b] : [b, a];
for (let i = lo; i <= hi; i++) selectedTrackIds.add(allItems[i]);
renderTree();
return;
}
}
// Plain click: clear selection and open track
selectedTrackIds.clear();
lastClickedTrackId = id;
renderTree();
openTrack(id);
});
el.addEventListener('mouseenter', () => MapView.highlightTrack(parseInt(el.dataset.id)));
el.addEventListener('mouseleave', () => MapView.unhighlightTrack());
});
container.querySelectorAll('.view-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => { e.stopPropagation(); openTrack(parseInt(btn.dataset.id)); });
});
container.querySelectorAll('.share-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => { e.stopPropagation(); shareTrack(parseInt(btn.dataset.id)); });
});
container.querySelectorAll('.edit-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
editTrack(parseInt(btn.dataset.id), btn.dataset.name, btn.dataset.type);
});
});
container.querySelectorAll('.move-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
showMoveDialog(parseInt(btn.dataset.id), btn.dataset.name);
});
});
container.querySelectorAll('.delete-track-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
confirmDelete('track', btn.dataset.name, () => deleteTrack(parseInt(btn.dataset.id)));
});
});
// Drag tracks
container.querySelectorAll('.track-item[draggable="true"]').forEach(el => {
el.addEventListener('dragstart', (e) => {
const id = parseInt(el.dataset.id);
if (selectedTrackIds.has(id)) {
dragTrackIds = [...selectedTrackIds];
} else {
selectedTrackIds.clear();
dragTrackIds = [id];
}
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(id));
container.querySelectorAll('.track-item').forEach(item => {
if (dragTrackIds.includes(parseInt(item.dataset.id))) item.classList.add('dragging');
});
});
el.addEventListener('dragend', () => {
container.querySelectorAll('.track-item.dragging').forEach(item => item.classList.remove('dragging'));
container.querySelectorAll('.drag-target').forEach(d => d.classList.remove('drag-target'));
dragTrackIds = [];
stopAutoScroll();
});
});
// Drag folders
container.querySelectorAll('.dir-item[draggable="true"]').forEach(el => {
el.addEventListener('dragstart', (e) => {
e.stopPropagation();
dragDirId = parseInt(el.dataset.id);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(dragDirId));
el.classList.add('dragging');
});
el.addEventListener('dragend', () => {
el.classList.remove('dragging');
container.querySelectorAll('.drag-target').forEach(d => d.classList.remove('drag-target'));
dragDirId = null;
stopAutoScroll();
});
});
// Auto-scroll when dragging near list edges
const list = document.getElementById('browser-list');
list.addEventListener('dragover', (e) => {
if (dragTrackIds.length === 0 && dragDirId === null) return;
const rect = list.getBoundingClientRect();
const ZONE = 48;
if (e.clientY < rect.top + ZONE) {
startAutoScroll(list, -1);
} else if (e.clientY > rect.bottom - ZONE) {
startAutoScroll(list, 1);
} else {
stopAutoScroll();
}
});
list.addEventListener('dragleave', (e) => {
if (!list.contains(e.relatedTarget)) stopAutoScroll();
});
// Drop targets (folders)
container.querySelectorAll('.dir-item[data-drop-target]').forEach(el => {
el._dragCount = 0;
function isDragging() { return dragTrackIds.length > 0 || dragDirId !== null; }
// For dir drags: reject drop onto self or own descendant
function isValidDirDrop(targetId) {
if (dragDirId === null) return false;
if (dragDirId === targetId) return false;
// Walk up from targetId through dirMeta; if we hit dragDirId it's a descendant
let cur = targetId;
const seen = new Set();
while (cur !== null && cur !== undefined) {
if (seen.has(cur)) break;
seen.add(cur);
const meta = dirMeta[cur];
if (!meta) break;
cur = meta.parentId;
if (cur === dragDirId) return false;
}
return true;
}
el.addEventListener('dragenter', (e) => {
if (!isDragging()) return;
if (dragDirId !== null && !isValidDirDrop(parseInt(el.dataset.id))) return;
e.preventDefault();
el._dragCount = (el._dragCount || 0) + 1;
el.classList.add('drag-target');
});
el.addEventListener('dragleave', () => {
if (!isDragging()) return;
el._dragCount = (el._dragCount || 1) - 1;
if (el._dragCount <= 0) {
el._dragCount = 0;
el.classList.remove('drag-target');
}
});
el.addEventListener('dragover', (e) => {
if (!isDragging()) return;
if (dragDirId !== null && !isValidDirDrop(parseInt(el.dataset.id))) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
el.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
el._dragCount = 0;
el.classList.remove('drag-target');
const targetDirId = parseInt(el.dataset.id);
const targetDirName = el.dataset.name;
if (dragTrackIds.length > 0) {
const ids = dragTrackIds.slice();
dragTrackIds = [];
stopAutoScroll();
confirmMoveTracks(ids, targetDirId, targetDirName);
} else if (dragDirId !== null && isValidDirDrop(targetDirId)) {
const srcId = dragDirId;
const srcName = dirMeta[srcId]?.name || 'folder';
dragDirId = null;
stopAutoScroll();
confirmMoveDir(srcId, srcName, targetDirId, targetDirName);
}
});
});
}
// ===== Auto-scroll helpers =====
function startAutoScroll(el, dir) {
if (autoScrollDir === dir) return;
autoScrollDir = dir;
stopAutoScroll();
function step() {
el.scrollTop += dir * 6;
autoScrollRaf = requestAnimationFrame(step);
}
autoScrollRaf = requestAnimationFrame(step);
}
function stopAutoScroll() {
autoScrollDir = 0;
if (autoScrollRaf !== null) {
cancelAnimationFrame(autoScrollRaf);
autoScrollRaf = null;
}
}
// ===== Expand / Select =====
async function toggleExpand(dirId) {
if (expandedDirs.has(dirId)) {
expandedDirs.delete(dirId);
renderTree();
} else {
expandedDirs.add(dirId);
if (!dirContents[dirId]) {
renderTree(); // show loading placeholder
try {
const data = await API.getDir(dirId);
dirMeta[data.id] = { id: data.id, name: data.name, parentId: data.parentId };
dirContents[dirId] = { dirs: data.children || [], tracks: data.tracks || [] };
(data.children || []).forEach(d => {
dirMeta[d.id] = { id: d.id, name: d.name, parentId: dirId };
});
} catch (e) {
showToast('Error: ' + e.message, 'error');
expandedDirs.delete(dirId);
}
}
renderTree();
}
}
async function selectDir(dirId) {
selectedDirId = dirId;
renderBreadcrumb();
renderTree();
// Ensure contents loaded
if (!dirContents[dirId]) {
try {
const data = await API.getDir(dirId);
dirMeta[data.id] = { id: data.id, name: data.name, parentId: data.parentId };
dirContents[dirId] = { dirs: data.children || [], tracks: data.tracks || [] };
(data.children || []).forEach(d => {
dirMeta[d.id] = { id: d.id, name: d.name, parentId: dirId };
});
} catch (e) {
showToast('Error loading folder: ' + e.message, 'error');
return;
}
}
// Load all tracks in this folder onto map and zoom
const tracks = dirContents[dirId].tracks;
if (tracks.length === 0) return;
await Promise.all(tracks.map(async track => {
if (!MapView.hasTrack(track.id)) {
try {
const data = await API.getTrackPoints(track.id);
MapView.addTrack(data, track.id);
} catch (e) { /* ignore individual errors */ }
}
}));
MapView.fitAll();
}
// ===== Track Actions =====
async function openTrack(trackId) {
try {
if (MapView.hasTrack(trackId)) {
MapView.removeTrack(trackId);
MapView.setCurrentTrack(null);
document.getElementById('track-info-panel').style.display = 'none';
if (typeof Elevation !== 'undefined') Elevation.clear();
return;
}
const data = await API.getTrackPoints(trackId);
MapView.addTrack(data, trackId);
MapView.fitTrack(trackId);
MapView.setCurrentTrack(trackId);
showTrackInfo(data.meta);
if (typeof Elevation !== 'undefined') {
const pts = MapView.getTrackPoints(trackId);
if (pts) Elevation.setTrack(pts, data.meta);
}
} catch (e) {
showToast('Error loading track: ' + e.message, 'error');
}
}
function showTrackInfo(meta) {
if (!meta) return;
const typeEmoji = TRACK_TYPE_EMOJI[meta.trackType];
const typeTitle = meta.trackType
? meta.trackType.charAt(0).toUpperCase() + meta.trackType.slice(1)
: '';
const emojiPrefix = typeEmoji
? `${typeEmoji}`
: '';
const panel = document.getElementById('track-info-panel');
document.getElementById('track-info-content').innerHTML = `
${emojiPrefix}${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'; };
}
function editTrack(trackId, currentName, currentType) {
const modal = document.getElementById('edit-track-modal');
document.getElementById('edit-track-name').value = currentName || '';
document.getElementById('edit-track-type').value = currentType || '';
modal.style.display = 'flex';
document.getElementById('edit-track-save-btn').onclick = async () => {
const name = document.getElementById('edit-track-name').value.trim();
const trackType = document.getElementById('edit-track-type').value || null;
if (!name) { showToast('Name cannot be empty', 'error'); return; }
modal.style.display = 'none';
try {
await API.updateTrack(trackId, { name, trackType });
await reload();
showToast('Track updated', 'success');
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
};
}
async function deleteTrack(id) {
try {
MapView.removeTrack(id);
await API.deleteTrack(id);
await reload();
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
// ===== Dir Actions =====
async function createDirPrompt() {
const name = prompt('Folder name:');
if (!name || !name.trim()) return;
try {
await API.createDir(name.trim(), selectedDirId);
await reload();
} 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 reload();
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
async function deleteDir(id) {
try {
await API.deleteDir(id);
if (selectedDirId === id) selectedDirId = null;
expandedDirs.delete(id);
await reload();
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
}
// ===== Upload =====
async function uploadFiles(files) {
if (files.length === 0) return;
const total = files.length;
let failed = 0;
for (let i = 0; i < files.length; i++) {
const file = files[i];
showUploadToast(total > 1
? `Uploading ${i + 1}/${total}: ${file.name}`
: `Uploading ${file.name}β¦`);
try {
await API.uploadTrack(file, selectedDirId, null);
} catch (err) {
failed++;
showToast(`Failed: ${file.name} β ${err.message}`, 'error');
// Let the error toast show briefly before moving on
await new Promise(r => setTimeout(r, 1200));
}
}
const ok = total - failed;
if (total === 1) {
failed ? hideUploadToast() : showToast('Uploaded successfully', 'success');
} else {
showToast(
failed === 0
? `Uploaded ${ok} file${ok !== 1 ? 's' : ''}`
: `Uploaded ${ok}/${total} β ${failed} failed`,
failed ? 'error' : 'success'
);
}
await reload();
}
async function handleFileUpload(e) {
const files = Array.from(e.target.files);
e.target.value = '';
await uploadFiles(files);
}
function setupDropZone() {
const mapContainer = document.getElementById('map-container');
mapContainer.addEventListener('dragover', (e) => {
// Only handle file drops (not track drags)
if (e.dataTransfer.types.includes('Files')) {
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'));
await uploadFiles(files);
});
}
// ===== Move Tracks =====
function confirmMoveTracks(trackIds, targetDirId, targetDirName) {
document.getElementById('confirm-title').textContent = 'Move Track' + (trackIds.length > 1 ? 's' : '');
document.getElementById('confirm-message').textContent = trackIds.length === 1
? `Move this track to folder "${targetDirName}"?`
: `Move ${trackIds.length} tracks to folder "${targetDirName}"?`;
const modal = document.getElementById('confirm-modal');
modal.style.display = 'flex';
document.getElementById('confirm-ok-btn').onclick = async () => {
modal.style.display = 'none';
try {
await Promise.all(trackIds.map(id => API.updateTrack(id, { directoryId: targetDirId })));
selectedTrackIds.clear();
await reload();
showToast(trackIds.length === 1 ? 'Track moved' : `${trackIds.length} tracks moved`, 'success');
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
};
}
function confirmMoveDir(srcId, srcName, targetDirId, targetDirName) {
document.getElementById('confirm-title').textContent = 'Move Folder';
document.getElementById('confirm-message').textContent =
`Move folder "${srcName}" into "${targetDirName}"?`;
const modal = document.getElementById('confirm-modal');
modal.style.display = 'flex';
document.getElementById('confirm-ok-btn').onclick = async () => {
modal.style.display = 'none';
try {
await API.moveDir(srcId, targetDirId);
await reload();
showToast('Folder moved', 'success');
} catch (e) {
showToast('Error: ' + e.message, 'error');
}
};
}
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 selectedMoveId = 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');
selectedMoveId = el.dataset.id || null;
});
});
document.getElementById('move-confirm-btn').onclick = async () => {
try {
await API.updateTrack(trackId, { directoryId: selectedMoveId || null });
document.getElementById('move-modal').style.display = 'none';
await reload();
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;
}
// ===== Share =====
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;
const appRoot = base.replace(/\/[^/]*$/, '/');
return appRoot + 'share/' + code;
}
// ===== Confirm Delete =====
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 selectedDirId; }
function refresh() { return reload(); }
return { init, getCurrentDirId, refresh };
})();