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 ? '