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(entry?.name || data.name || 'Vorschau')} + ${escapeHtml(formatBytes(data.size || entry?.size || 0))} +
+
${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 @@ https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -b74c325d1c332768da07a6ab11de7a03 +2c27aa33b2a3be760a518625a0762cac