diff --git a/build-plugin.sh b/build-plugin.sh index d83fb4a..b7d6773 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -5,7 +5,7 @@ set -eu export COPYFILE_DISABLE=1 PLUGIN="u-navigator" -VERSION="2026.07.14.r017" +VERSION="2026.09.12.r001" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.09.12.r001.tgz b/packages/u-navigator-2026.09.12.r001.tgz new file mode 100644 index 0000000..847144f Binary files /dev/null and b/packages/u-navigator-2026.09.12.r001.tgz differ diff --git a/server/public/app.js b/server/public/app.js index 99f67c7..62e895c 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -126,6 +126,7 @@ function openExplorer(initialPath = state.config?.roots?.[0]?.path) { selectedPaths: [], selectionAnchorPath: null, view: 'icons', + sort: { key: 'name', dir: 1 }, treeVisible: true, treeWidth: 220, treeExpanded: [], @@ -349,7 +350,7 @@ function renderExplorer(body, win) {
${data.error ? `

${escapeHtml(data.error)}

` : ''} - ${data.loading ? '

Lade...

' : renderFileEntries(data.entries || [], data.view || 'list', selectedPathSet(win))} + ${data.loading ? '

Lade...

' : renderFileEntries(data.entries || [], data.view || 'list', selectedPathSet(win), data.sort || { key: 'name', dir: 1 })}
@@ -365,6 +366,14 @@ function renderExplorer(body, win) { }); body.querySelector('[data-action="view-list"]').addEventListener('click', () => setExplorerView(win, 'list')); body.querySelector('[data-action="view-icons"]').addEventListener('click', () => setExplorerView(win, 'icons')); + for (const th of body.querySelectorAll('.file-table th[data-sort-key]')) { + th.addEventListener('click', () => { + const key = th.dataset.sortKey; + const current = data.sort || { key: 'name', dir: 1 }; + data.sort = current.key === key ? { key, dir: -current.dir } : { key, dir: 1 }; + render(); + }); + } body.querySelector('[data-action="upload-files"]').addEventListener('click', () => body.querySelector('[data-upload="files"]').click()); body.querySelector('[data-action="upload-folder"]').addEventListener('click', () => body.querySelector('[data-upload="folder"]').click()); body.querySelector('[data-action="delete-selected"]').addEventListener('click', () => deleteEntries(win)); @@ -562,14 +571,37 @@ function startTreeResize(event, win, body) { handle.addEventListener('pointercancel', up); } -function renderFileEntries(entries, view, selectedPaths = new Set()) { +function sortEntries(entries, sort) { + const key = sort?.key || 'name'; + const dir = sort?.dir === -1 ? -1 : 1; + const typeOrder = { directory: 0, file: 1, symlink: 2, other: 3 }; + const compareValue = (entry) => { + if (key === 'size') return entry.type === 'file' ? (entry.size || 0) : -1; + if (key === 'modifiedAt') return Date.parse(entry.modifiedAt) || 0; + if (key === 'type') return typeOrder[entry.type] ?? 4; + return entry.name || ''; + }; + const compareName = (a, b) => (a.name || '').localeCompare(b.name || undefined, undefined, { numeric: true, sensitivity: 'base' }); + return [...entries].sort((a, b) => { + // Ordner immer zuerst, dann Dateien, dann Verknüpfungen/sonstige + if (a.type !== b.type) return (typeOrder[a.type] ?? 4) - (typeOrder[b.type] ?? 4); + if (key === 'name') return dir * compareName(a, b); + const valueA = compareValue(a); + const valueB = compareValue(b); + if (valueA !== valueB) return dir * (valueA < valueB ? -1 : 1); + return compareName(a, b); + }); +} + +function renderFileEntries(entries, view, selectedPaths = new Set(), sort = { key: 'name', dir: 1 }) { if (!entries.length) { return '

Dieser Ordner ist leer. Dateien können hier abgelegt werden.

'; } - return view === 'icons' ? renderFileIcons(entries, selectedPaths) : renderFileTable(entries, selectedPaths); + const sorted = sortEntries(entries, sort); + return view === 'icons' ? renderFileIcons(sorted, selectedPaths) : renderFileTable(sorted, selectedPaths, sort); } -function renderFileTable(entries, selectedPaths) { +function renderFileTable(entries, selectedPaths, sort = { key: 'name', dir: 1 }) { const rows = entries.map((entry) => ` ${fileTypeIcon(entry)}${escapeHtml(entry.name)} @@ -579,9 +611,15 @@ function renderFileTable(entries, selectedPaths) { `).join(''); + const header = (key, label) => { + const active = sort.key === key; + const arrow = active ? (sort.dir === 1 ? ' ▲' : ' ▼') : ''; + return `${label}${arrow}`; + }; + return ` - + ${header('name', 'Name')}${header('type', 'Typ')}${header('size', 'Größe')}${header('modifiedAt', 'Geändert')}${rows}
NameTypGrößeGeändert
`; diff --git a/server/public/styles.css b/server/public/styles.css index 3667c9e..7253760 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -573,6 +573,21 @@ color: var(--muted); font-size: 11px; font-weight: 600; + cursor: pointer; + user-select: none; + white-space: nowrap; +} + +.u-nav .file-table th:hover { + color: var(--text); +} + +.u-nav .file-table th.sorted { + color: var(--accent); +} + +.u-nav .file-table th .sort-arrow { + font-size: 9px; } .u-nav .file-row { diff --git a/u-navigator.plg b/u-navigator.plg index e61897b..dbf2c2d 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + @@ -24,7 +24,7 @@ https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -571b9c735dfab01e7d62736bf517dc45 +9cee8533bb7f227236fb9ff4963e2d73