diff --git a/build-plugin.sh b/build-plugin.sh index f618596..9e32399 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r028" +VERSION="2026.06.23.r029" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r028.tgz b/packages/u-navigator-2026.06.23.r028.tgz deleted file mode 100644 index 8fcb306..0000000 Binary files a/packages/u-navigator-2026.06.23.r028.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r029.tgz b/packages/u-navigator-2026.06.23.r029.tgz new file mode 100644 index 0000000..e060eb6 Binary files /dev/null and b/packages/u-navigator-2026.06.23.r029.tgz differ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/preview.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/preview.php new file mode 100644 index 0000000..193affd --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/preview.php @@ -0,0 +1,39 @@ + UNAV_PREVIEW_MAX_BYTES) { + unav_error(413, 'Preview file is larger than 1 MB'); +} + +$content = file_get_contents($resolved['path']); +if ($content === false) { + unav_error(500, 'Could not read file'); +} +if (strpos($content, "\0") !== false) { + unav_error(415, 'Preview is not available for binary files'); +} + +unav_json([ + 'path' => $resolved['path'], + 'name' => basename($resolved['path']), + 'size' => $size, + 'extension' => $extension, + 'content' => $content, +]); diff --git a/server/public/app.js b/server/public/app.js index 1ab5453..b2ee669 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -35,6 +35,7 @@ const icons = { symlink: 'Link', other: 'Item' }; +const previewExtensions = new Set(['txt', 'log', 'md', 'rd', 'rst', 'yaml', 'yml', 'json', 'xml', 'csv', 'ini', 'cfg', 'conf', 'sh', 'php', 'js', 'css', 'html']); document.querySelector('#newExplorerButton').addEventListener('click', () => openExplorer()); document.querySelector('#newPropertiesButton').addEventListener('click', () => openProperties()); @@ -112,6 +113,40 @@ function openProperties(entry = state.selectedEntry) { }); } +function openPreview(entry) { + if (!entry || !isPreviewable(entry)) { + showError(new Error('Für diesen Dateityp ist keine Vorschau verfügbar.')); + return null; + } + + const existing = state.windows.find((item) => item.kind === 'preview'); + if (existing) { + existing.data.entry = entry; + existing.data.content = ''; + existing.data.loading = true; + existing.data.error = null; + focusWindow(existing); + render(); + loadPreview(existing, entry); + return existing; + } + + const win = createWindow('Vorschau', 'preview', { + width: 620, + height: 460, + x: 190, + y: 110, + data: { + entry, + content: '', + loading: true, + error: null + } + }); + loadPreview(win, entry); + return win; +} + function openQueue() { showTransferPanel(); return null; @@ -187,6 +222,7 @@ function renderWindow(win) { body.className = 'window-body'; if (win.kind === 'explorer') renderExplorer(body, win); if (win.kind === 'properties') renderProperties(body); + if (win.kind === 'preview') renderPreview(body, win); body.addEventListener('scroll', () => { if (win.data.lockScroll) { body.scrollTop = win.data.lockScrollTop || 0; @@ -289,6 +325,8 @@ function renderExplorer(body, win) { if (shouldSuppressItemActivation()) return; if (entry.type === 'directory') { loadExplorer(win, entry.path); + } else if (isPreviewable(entry)) { + openPreview(entry); } }); row.addEventListener('contextmenu', (event) => { @@ -368,6 +406,45 @@ function renderProperties(body) { } } +function renderPreview(body, win) { + const data = win.data; + body.classList.add('preview-body'); + if (data.loading) { + body.innerHTML = '
Lade Vorschau...
'; + return; + } + if (data.error) { + body.innerHTML = `${escapeHtml(data.error)}
`; + return; + } + const entry = data.entry; + body.innerHTML = ` + +${escapeHtml(data.content || '')}
+ `;
+}
+
+async function loadPreview(win, entry) {
+ win.data.loading = true;
+ win.data.error = null;
+ render();
+ try {
+ const result = await api(apiUrl(`preview.php?path=${encodeURIComponent(entry.path)}`));
+ win.data.entry = entry;
+ win.data.name = result.name;
+ win.data.size = result.size;
+ win.data.content = result.content;
+ } catch (error) {
+ win.data.error = error.message;
+ } finally {
+ win.data.loading = false;
+ render();
+ }
+}
+
function renderQueue(body) {
body.innerHTML = queueMarkup();
bindQueueActions(body);
@@ -797,6 +874,8 @@ function renderContextMenu() {
if (entry) {
if (entry.type === 'directory') {
actions.push(['Öffnen', () => loadExplorer(win, entry.path)]);
+ } else if (isPreviewable(entry)) {
+ actions.push(['Vorschau', () => openPreview(entry)]);
}
actions.push(['Eigenschaften', () => showPropertiesForEntry(win, entry)]);
if (!isReadOnly()) {
@@ -1258,6 +1337,9 @@ function windowTitle(win) {
if (win.kind === 'explorer') {
return `Explorer · ${win.data.path || ''}`;
}
+ if (win.kind === 'preview') {
+ return `Vorschau · ${win.data.entry?.name || ''}`;
+ }
return win.title;
}
@@ -1273,6 +1355,16 @@ function dirname(value) {
return `/${parts.slice(0, -1).join('/')}`;
}
+function extensionOf(name) {
+ const value = String(name || '');
+ const index = value.lastIndexOf('.');
+ return index >= 0 ? value.slice(index + 1).toLowerCase() : '';
+}
+
+function isPreviewable(entry) {
+ return entry?.type === 'file' && previewExtensions.has(extensionOf(entry.name || entry.path));
+}
+
function joinPath(parent, name) {
return `${String(parent || '').replace(/\/+$/, '')}/${String(name || '').replace(/^\/+/, '')}`;
}
diff --git a/server/public/styles.css b/server/public/styles.css
index 5796fa4..a871671 100644
--- a/server/public/styles.css
+++ b/server/public/styles.css
@@ -694,6 +694,43 @@
overflow-wrap: anywhere;
}
+.u-nav .preview-body {
+ display: grid;
+ gap: 10px;
+ grid-template-rows: auto minmax(0, 1fr);
+}
+
+.u-nav .preview-meta {
+ align-items: center;
+ border-bottom: 1px solid var(--line);
+ color: var(--muted);
+ display: flex;
+ gap: 10px;
+ justify-content: space-between;
+ padding-bottom: 8px;
+}
+
+.u-nav .preview-meta strong {
+ color: var(--text);
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.u-nav .preview-text {
+ background: var(--surface-2);
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ color: var(--text);
+ font: 13px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+ margin: 0;
+ min-height: 0;
+ overflow: auto;
+ padding: 10px;
+ white-space: pre;
+}
+
@media (max-width: 720px) {
.u-nav .topbar {
align-items: flex-start;
diff --git a/u-navigator.plg b/u-navigator.plg
index ccf5087..f890578 100644
--- a/u-navigator.plg
+++ b/u-navigator.plg
@@ -1,7 +1,7 @@
-
+
@@ -24,7 +24,7 @@