feat: sortierbare Spalten in der Detailansicht
Klick auf die Tabellen-Header (Name, Typ, Größe, Datum) sortiert die Dateiliste pro Fenster; zweiter Klick wechselt die Richtung. Ordner bleiben stets oben, Natural Sort für Dateinamen. - app.js: sortEntries(), sortierbare th-Header mit ▲/▼, Klick-Handler - styles.css: Header-Hover, hervorgehobene aktive Spalte - Build auf 2026.09.12.r001 (Version + MD5 im .plg)
This commit is contained in:
+1
-1
@@ -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"
|
||||
|
||||
Binary file not shown.
+43
-5
@@ -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) {
|
||||
<div class="explorer-content">
|
||||
<div class="drop-zone ${data.dragOver ? 'drag-over' : ''}">
|
||||
${data.error ? `<p class="muted explorer-message">${escapeHtml(data.error)}</p>` : ''}
|
||||
${data.loading ? '<p class="muted explorer-message">Lade...</p>' : renderFileEntries(data.entries || [], data.view || 'list', selectedPathSet(win))}
|
||||
${data.loading ? '<p class="muted explorer-message">Lade...</p>' : renderFileEntries(data.entries || [], data.view || 'list', selectedPathSet(win), data.sort || { key: 'name', dir: 1 })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 '<p class="muted">Dieser Ordner ist leer. Dateien können hier abgelegt werden.</p>';
|
||||
}
|
||||
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) => `
|
||||
<tr class="file-row ${selectedPaths.has(entry.path) ? 'selected' : ''}" data-path="${escapeAttr(entry.path)}" data-type="${escapeAttr(entry.type)}">
|
||||
<td><span class="name-cell">${fileTypeIcon(entry)}<span>${escapeHtml(entry.name)}</span></span></td>
|
||||
@@ -579,9 +611,15 @@ function renderFileTable(entries, selectedPaths) {
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
const header = (key, label) => {
|
||||
const active = sort.key === key;
|
||||
const arrow = active ? (sort.dir === 1 ? ' <span class="sort-arrow">▲</span>' : ' <span class="sort-arrow">▼</span>') : '';
|
||||
return `<th data-sort-key="${key}" class="${active ? 'sorted' : ''}" role="columnheader" aria-sort="${active ? (sort.dir === 1 ? 'ascending' : 'descending') : 'none'}" title="${active ? 'Richtung wechseln' : `Nach ${label} sortieren`}">${label}${arrow}</th>`;
|
||||
};
|
||||
|
||||
return `
|
||||
<table class="file-table">
|
||||
<thead><tr><th>Name</th><th>Typ</th><th>Größe</th><th>Geändert</th></tr></thead>
|
||||
<thead><tr>${header('name', 'Name')}${header('type', 'Typ')}${header('size', 'Größe')}${header('modifiedAt', 'Geändert')}</tr></thead>
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE PLUGIN [
|
||||
<!ENTITY name "u-navigator">
|
||||
<!ENTITY author "Michael Roll">
|
||||
<!ENTITY version "2026.07.14.r017">
|
||||
<!ENTITY version "2026.09.12.r001">
|
||||
<!ENTITY package "&name;-&version;.tgz">
|
||||
<!ENTITY pluginURL "https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
|
||||
<!ENTITY support "https://git.casaderoll.de/michael/Unraid-Navigator">
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<FILE Name="/boot/config/plugins/&name;/&package;">
|
||||
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
|
||||
<MD5>571b9c735dfab01e7d62736bf517dc45</MD5>
|
||||
<MD5>9cee8533bb7f227236fb9ff4963e2d73</MD5>
|
||||
</FILE>
|
||||
|
||||
<FILE Run="/bin/bash">
|
||||
|
||||
Reference in New Issue
Block a user