atualiza versão para 0.0.5; adiciona modal de navegador de diretórios e nova rota API para listar subdiretórios

This commit is contained in:
Alexander Sabino
2026-05-09 16:22:59 +01:00
parent 971293cacf
commit 77a6179296
6 changed files with 231 additions and 5 deletions
+62
View File
@@ -239,6 +239,68 @@ elements.storageLocationsList?.addEventListener('click', async (e) => {
}
});
// ─── Directory Browser Modal ──────────────────────────────
let _dirBrowserCurrentPath = '/';
function openDirBrowser() {
const initial = elements.storageLocationDir.value.trim() || '/';
_dirBrowserCurrentPath = initial;
const modal = document.querySelector('#dirBrowserModal');
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden', 'false');
loadDirBrowserPath(initial);
}
function closeDirBrowser() {
const modal = document.querySelector('#dirBrowserModal');
modal.classList.add('hidden');
modal.setAttribute('aria-hidden', 'true');
}
async function loadDirBrowserPath(dirPath) {
_dirBrowserCurrentPath = dirPath;
document.querySelector('#dirBrowserCurrent').textContent = dirPath;
const list = document.querySelector('#dirBrowserList');
list.innerHTML = '<div class="dir-browser-empty">Carregando…</div>';
let data;
try {
data = await api(`/api/browse-dirs?path=${encodeURIComponent(dirPath)}`);
} catch (err) {
list.innerHTML = `<div class="dir-browser-empty">${escapeHtml(err.message)}</div>`;
return;
}
const folderSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
const upSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><polyline points="15 18 9 12 15 6"/></svg>`;
let html = '';
if (data.parent !== null) {
html += `<div class="dir-browser-item dir-browser-item--up" data-dir-path="${escapeHtml(data.parent)}">${upSvg}<span>.. (subir)</span></div>`;
}
if (data.dirs.length === 0 && data.parent === null) {
html += '<div class="dir-browser-empty">Nenhum subdiretório encontrado.</div>';
} else {
html += data.dirs.map((d) =>
`<div class="dir-browser-item" data-dir-path="${escapeHtml(d.path)}">${folderSvg}<span>${escapeHtml(d.name)}</span></div>`
).join('');
}
list.innerHTML = html;
list.querySelectorAll('.dir-browser-item[data-dir-path]').forEach((item) => {
item.addEventListener('click', () => loadDirBrowserPath(item.dataset.dirPath));
});
}
document.querySelector('#browseDirBtn')?.addEventListener('click', openDirBrowser);
document.querySelector('#dirBrowserClose')?.addEventListener('click', closeDirBrowser);
document.querySelector('#dirBrowserCancel')?.addEventListener('click', closeDirBrowser);
document.querySelector('#dirBrowserBackdrop')?.addEventListener('click', closeDirBrowser);
document.querySelector('#dirBrowserSelect')?.addEventListener('click', () => {
elements.storageLocationDir.value = _dirBrowserCurrentPath;
closeDirBrowser();
});
// ─── Full Backup Picker Modal ─────────────────────────────
function askFullBackupSelection(fullBackups, profileName) {
elements.fullBackupPickerOptions.innerHTML = fullBackups.map((b) => `
+26 -1
View File
@@ -453,7 +453,12 @@
</div>
<div class="form-field">
<label for="storageLocationDir">Diretório</label>
<input id="storageLocationDir" type="text" placeholder="/srv/docker-backups" required />
<div class="dir-input-group">
<input id="storageLocationDir" type="text" placeholder="/srv/docker-backups" required />
<button type="button" id="browseDirBtn" class="btn btn--secondary btn--sm dir-browse-btn" title="Procurar diretório">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
</button>
</div>
</div>
<div class="form-actions">
<button class="btn btn--primary" type="submit">Salvar</button>
@@ -463,6 +468,26 @@
</div>
</div>
<!-- Modal: Navegador de Diretórios -->
<div id="dirBrowserModal" class="modal hidden" aria-hidden="true">
<div class="modal-backdrop" id="dirBrowserBackdrop"></div>
<div class="modal-card dir-browser-card" role="dialog" aria-modal="true" aria-labelledby="dirBrowserTitle">
<div class="modal-header">
<h3 id="dirBrowserTitle">Selecionar Diretório</h3>
<button id="dirBrowserClose" class="btn btn--ghost btn--sm" type="button">Fechar</button>
</div>
<div class="dir-browser-breadcrumb">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
<span id="dirBrowserCurrent" class="dir-browser-current-path"></span>
</div>
<div id="dirBrowserList" class="dir-browser-list"></div>
<div class="modal-actions">
<button id="dirBrowserSelect" class="btn btn--primary" type="button">Selecionar este diretório</button>
<button id="dirBrowserCancel" class="btn btn--secondary" type="button">Cancelar</button>
</div>
</div>
</div>
<script src="/translations.js"></script>
<script src="/app.js"></script>
</body>
+105
View File
@@ -1109,6 +1109,111 @@ button, input, select {
.volume-blocked { opacity: 0.5; cursor: not-allowed; }
/* --- Dir Input Group ------------------------------------- */
.dir-input-group {
display: flex;
gap: 6px;
align-items: center;
}
.dir-input-group input {
flex: 1;
min-width: 0;
}
.dir-browse-btn {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 10px;
}
/* --- Dir Browser Modal ----------------------------------- */
#dirBrowserModal {
z-index: 80;
}
.dir-browser-card {
width: min(520px, calc(100vw - 24px));
max-height: min(75vh, 560px);
display: flex;
flex-direction: column;
gap: 10px;
}
.dir-browser-breadcrumb {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 12px;
color: var(--text-muted);
min-height: 34px;
}
.dir-browser-current-path {
font-family: 'IBM Plex Mono', monospace;
font-size: 12px;
color: var(--text);
word-break: break-all;
}
.dir-browser-list {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2px;
min-height: 120px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 4px;
}
.dir-browser-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
color: var(--text);
transition: background 0.1s;
user-select: none;
}
.dir-browser-item:hover {
background: var(--bg);
}
.dir-browser-item--up {
color: var(--text-muted);
font-style: italic;
}
.dir-browser-item svg {
flex-shrink: 0;
color: var(--accent, #3b82f6);
}
.dir-browser-item--up svg {
color: var(--text-muted);
}
.dir-browser-empty {
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 13px;
padding: 24px 0;
}
/* --- Responsive ------------------------------------------ */
@media (max-width: 900px) {
:root { --sidebar-w: 60px; }