Improve folder tree usability

This commit is contained in:
Mikei386
2026-06-21 16:20:10 +02:00
parent 2b6b629db9
commit 7dd0685a5a
8 changed files with 40 additions and 17 deletions
-1
View File
@@ -1 +0,0 @@
7b3b3c9fd00ce2aae592ea4ec84d2875afce72ca2cad2f24a7e075312a6c528f urbm-2026.06.21.r007-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
b571e6ee3cca59d6f63e15c721d53d04f52d27e4e121fa1d314dde7fe7d454cc urbm-2026.06.21.r008-x86_64-1.txz
+6 -2
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [
<!ENTITY name "urbm">
<!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.21.r007">
<!ENTITY version "2026.06.21.r008">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "7b3b3c9fd00ce2aae592ea4ec84d2875afce72ca2cad2f24a7e075312a6c528f">
<!ENTITY packageSHA256 "b571e6ee3cca59d6f63e15c721d53d04f52d27e4e121fa1d314dde7fe7d454cc">
]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES>
### 2026.06.21.r008
- Improve the visual hierarchy of folder trees with clearer disclosure buttons, row spacing, and normal path casing.
- Preserve tree and page scroll positions when expanding folders in directory and snapshot browsers.
### 2026.06.21.r007
- Show an indeterminate progress bar while a complete snapshot file tree is loading.
- Explain that no real percentage is available because Restic returns the full file listing only after completion.
+5 -1
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [
<!ENTITY name "urbm">
<!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.21.r007">
<!ENTITY version "2026.06.21.r008">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES>
### 2026.06.21.r008
- Improve the visual hierarchy of folder trees with clearer disclosure buttons, row spacing, and normal path casing.
- Preserve tree and page scroll positions when expanding folders in directory and snapshot browsers.
### 2026.06.21.r007
- Show an indeterminate progress bar while a complete snapshot file tree is loading.
- Explain that no real percentage is available because Restic returns the full file listing only after completion.
+4 -4
View File
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
<?php
$pluginRoot = '/plugins/urbm';
?>
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260621r007">
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260621r008">
<div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php">
<header class="bu-header">
<div class="bu-brand">
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260621r007" alt="URBM-Logo">
<div><h1>URBM <span class="bu-version">2026.06.21.r007</span></h1><p>Restic Backup Manager für Unraid</p></div>
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260621r008" alt="URBM-Logo">
<div><h1>URBM <span class="bu-version">2026.06.21.r008</span></h1><p>Restic Backup Manager für Unraid</p></div>
</div>
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div>
</header>
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
</div>
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.21.r007.js"></script>
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.21.r008.js"></script>
+9 -7
View File
@@ -84,18 +84,20 @@
.bu-source-partial .bu-source-select { accent-color: var(--bu-accent); }
.bu-partial-label { border: 1px solid var(--bu-accent); border-radius: 999px; color: var(--bu-text); display: inline-block; font-size: 11px; font-weight: 700; margin-left: 8px; padding: 2px 7px; vertical-align: middle; }
.bu-inherited-label { border: 1px solid var(--bu-good); border-radius: 999px; color: var(--bu-good); display: inline-block; font-size: 11px; font-weight: 700; margin-left: 8px; padding: 2px 7px; vertical-align: middle; }
.bu-tree { border: 1px solid var(--bu-border); border-radius: 6px; max-height: 520px; overflow: auto; padding: 5px; }
.bu-tree { background: var(--bu-input); border: 1px solid var(--bu-border); border-radius: 6px; max-height: 520px; overflow: auto; padding: 6px; }
.bu-tree-node { min-width: 0; }
.bu-tree-row { align-items: center; border-radius: 5px; display: grid; gap: 6px; grid-template-columns: 28px 24px minmax(0, 1fr); margin-left: calc(var(--bu-tree-depth) * 22px); min-height: 42px; padding: 3px 7px; }
.bu-dir-tree-row { grid-template-columns: 28px minmax(0, 1fr) auto; }
.bu-tree-node + .bu-tree-node { border-top: 1px solid color-mix(in srgb, var(--bu-border) 45%, transparent); }
.bu-tree-row { align-items: center; border-radius: 6px; display: grid; gap: 9px; grid-template-columns: 34px 24px minmax(0, 1fr); margin-left: calc(var(--bu-tree-depth) * 24px); min-height: 48px; padding: 5px 8px; }
.bu-dir-tree-row { grid-template-columns: 34px minmax(0, 1fr) auto; }
.bu-tree-row:hover { background: var(--bu-panel-alt); }
.bu-source-partial > .bu-tree-row { background: rgba(255, 140, 47, .09); background: color-mix(in srgb, var(--bu-accent) 9%, transparent); }
#urbm-app .bu-tree-toggle, #urbm-app .bu-tree-name { appearance: none; background: transparent !important; border: 0 !important; box-shadow: none !important; color: var(--bu-text) !important; margin: 0 !important; min-height: 0; padding: 0 !important; text-align: left; text-shadow: none !important; }
#urbm-app .bu-tree-toggle { align-items: center; cursor: pointer; display: flex; height: 28px; justify-content: center; width: 28px; }
.bu-tree-toggle span { display: inline-block; font-size: 14px; transition: transform .16s ease; }
#urbm-app .bu-tree-toggle { align-items: center; background: var(--bu-panel-alt) !important; border: 1px solid var(--bu-border) !important; border-radius: 6px !important; cursor: pointer; display: flex; height: 30px; justify-content: center; width: 30px; }
#urbm-app .bu-tree-toggle:hover { border-color: var(--bu-accent) !important; }
.bu-tree-toggle span { display: inline-block; font-size: 13px; line-height: 1; transition: transform .16s ease; }
.bu-tree-toggle.expanded span { transform: rotate(90deg); }
#urbm-app .bu-tree-name { cursor: pointer; display: grid; gap: 1px; grid-template-columns: auto minmax(0, 1fr); min-width: 0; }
.bu-tree-name > span:first-child { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#urbm-app .bu-tree-name { cursor: pointer; display: grid; gap: 2px; grid-template-columns: auto minmax(0, 1fr); letter-spacing: normal !important; min-width: 0; text-transform: none !important; }
.bu-tree-name > span:first-child { font-weight: 700; letter-spacing: normal; overflow: hidden; text-overflow: ellipsis; text-transform: none; white-space: nowrap; }
.bu-tree-name small { color: var(--bu-muted); grid-column: 1 / -1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bu-tree-children { position: relative; }
.bu-tree-children::before { background: var(--bu-border); bottom: 3px; content: ''; left: calc((var(--bu-tree-depth, 0) + 1) * 22px + 18px); opacity: .45; position: absolute; top: 0; width: 1px; }
+15 -2
View File
@@ -510,11 +510,15 @@
function renderSourceBrowser() {
const host=document.getElementById('bu-source-browser');
if(!host) return;
const windowX=window.scrollX, windowY=window.scrollY;
const previousScroll=host.querySelector('.bu-tree')?.scrollTop || 0;
const chosen=state.backupSelections.length?state.backupSelections.map(path=>`<span class="bu-selection">${esc(path)}</span>`).join(''):'<span class="bu-muted">Keine Ordner ausgewählt.</span>';
const tree=state.sourceTreeRoots.map(item=>sourceTreeNode(item,0)).join('');
host.innerHTML=`<section class="bu-browser-card"><div class="bu-toolbar"><div><h3>Backup-Quellen auswählen</h3><span class="bu-muted">Ordner mit dem Dreieck aufklappen. Unterordner werden erst bei Bedarf geladen.</span></div><span class="bu-count">Ausgewählt: ${state.backupSelections.length}</span></div><div class="bu-selection-list">${chosen}</div><div class="bu-tree" role="tree">${tree||'<div class="bu-empty">Keine Ordner verfügbar.</div>'}</div></section>`;
const treeElement=host.querySelector('.bu-tree'); if(treeElement) treeElement.scrollTop=previousScroll;
host.querySelectorAll('.bu-source-select[data-partial="true"]').forEach(box=>{box.indeterminate=true;box.setAttribute('aria-checked','mixed');});
enhanceTooltips(host);
window.scrollTo(windowX, windowY);
}
async function toggleSourceTree(path, forceOpen=false) {
@@ -558,9 +562,13 @@
function renderDirectoryTree(key) {
const tree=state.directoryTrees[key]; const host=document.getElementById(`bu-dir-tree-${key}`);
if(!tree||!host) return;
const windowX=window.scrollX, windowY=window.scrollY;
const previousScroll=host.querySelector('.bu-tree')?.scrollTop || 0;
const html=tree.roots.map(item=>directoryTreeNode(key,item,0)).join('');
host.innerHTML=`<section class="bu-browser-card"><div class="bu-toolbar"><div><h3>${esc(tree.title)}</h3><span class="bu-muted">Ordner mit dem Dreieck aufklappen und den gewünschten Zielordner auswählen.</span></div></div><div class="bu-tree" role="tree">${html||'<div class="bu-empty">Keine Ordner verfügbar.</div>'}</div></section>`;
const treeElement=host.querySelector('.bu-tree'); if(treeElement) treeElement.scrollTop=previousScroll;
enhanceTooltips(host);
window.scrollTo(windowX, windowY);
}
async function toggleDirectoryTree(key, path, forceOpen=false) {
@@ -738,6 +746,11 @@
notify('Konfiguration gespeichert');
return state.config;
}
function renderPreservingScroll() {
const windowX=window.scrollX, windowY=window.scrollY;
render();
window.scrollTo(windowX, windowY);
}
const lines = (value) => value.split('\n').map(v=>v.trim()).filter(Boolean);
const runTime = run => new Date(run.createdAt || run.startedAt || run.finishedAt || 0).getTime() || 0;
const snapshotTime = snapshot => new Date(snapshot.time || 0).getTime() || 0;
@@ -773,7 +786,7 @@
if (name === 'toggle-dir-tree') { return toggleDirectoryTree(a, decodeURIComponent(b)); }
if (name === 'retry-dir-tree') { const path=decodeURIComponent(b); const tree=state.directoryTrees[a]; if(tree) { delete tree.children[path]; tree.errors[path]=''; } return toggleDirectoryTree(a,path,true); }
if (name === 'select-dir-tree') { selectDirectoryTreePath(a, decodeURIComponent(b)); return; }
if (name === 'toggle-snapshot-tree') { const path=decodeURIComponent(a); state.snapshotTreeExpanded[path]=!state.snapshotTreeExpanded[path]; return render(); }
if (name === 'toggle-snapshot-tree') { const path=decodeURIComponent(a); state.snapshotTreeExpanded[path]=!state.snapshotTreeExpanded[path]; return renderPreservingScroll(); }
if (name === 'restore-selected') { if(!state.restoreIncludes.length) throw new Error('Wähle mindestens eine Datei oder einen Ordner aus.'); state.view='restore'; document.querySelectorAll('.bu-tabs button').forEach(x=>x.classList.toggle('active',x.dataset.view==='restore')); return render(); }
if (name === 'delete-job') { if(confirm('Diesen Job löschen?')) { state.config.jobs=state.config.jobs.filter(j=>j.id!==a); await saveConfig(); render(); } return; }
if (name === 'duplicate-job') { const source=state.config.jobs.find(j=>j.id===a); const copy=structuredClone(source); copy.id=id('job'); copy.name += ' Kopie'; copy.enabled=false; state.config.jobs.push(copy); await saveConfig(); return render(); }
@@ -879,7 +892,7 @@
if (e.target.classList.contains('bu-file-select')) {
const path=e.target.value; const prefix=path.replace(/\/$/,'')+'/';
state.restoreIncludes = e.target.checked ? normalizeSourceSelections([...state.restoreIncludes.filter(selected=>!selected.startsWith(prefix)),path]) : state.restoreIncludes.filter(selected=>selected!==path);
render();
renderPreservingScroll();
}
if (e.target.classList.contains('bu-workload-select')) {
state.workloadSelections = e.target.checked ? [...new Set([...state.workloadSelections,e.target.value])] : state.workloadSelections.filter(id=>id!==e.target.value);