diff --git a/build-plugin.sh b/build-plugin.sh index 2924131..60228e6 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r030" +VERSION="2026.06.23.r031" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r030.tgz b/packages/u-navigator-2026.06.23.r030.tgz deleted file mode 100644 index 5bbc173..0000000 Binary files a/packages/u-navigator-2026.06.23.r030.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r031.tgz b/packages/u-navigator-2026.06.23.r031.tgz new file mode 100644 index 0000000..341071b Binary files /dev/null and b/packages/u-navigator-2026.06.23.r031.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 index 193affd..1e8e10a 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/preview.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/preview.php @@ -1,16 +1,17 @@ UNAV_PREVIEW_MAX_BYTES) { - unav_error(413, 'Preview file is larger than 1 MB'); + +$mime = unav_preview_mime($extension, $type); +if (($_GET['raw'] ?? '') === '1') { + header('Content-Type: ' . $mime); + header('Content-Length: ' . $size); + header('X-Content-Type-Options: nosniff'); + readfile($resolved['path']); + exit; } -$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([ +$payload = [ 'path' => $resolved['path'], - 'name' => basename($resolved['path']), + 'name' => $name, 'size' => $size, 'extension' => $extension, - 'content' => $content, -]); + 'type' => $type, + 'mime' => $mime, + 'rawUrl' => 'preview.php?raw=1&path=' . rawurlencode($resolved['path']), +]; + +if (in_array($type, ['text', 'markdown', 'csv'], true)) { + if ($size > UNAV_TEXT_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'); + } + $payload['content'] = $content; +} + +unav_json($payload); + +function unav_preview_type(string $extension, string $name): ?string { + $base = strtolower($name); + if (in_array($base, ['dockerfile', 'makefile'], true)) { + return 'text'; + } + $text = ['txt', 'log', 'rd', 'rst', 'yaml', 'yml', 'json', 'xml', 'ini', 'cfg', 'conf', 'env', 'toml', 'sql', 'nfo', 'srt', 'sub', 'vtt', 'm3u', 'm3u8', 'pls', 'sh', 'bash', 'zsh', 'php', 'js', 'ts', 'tsx', 'jsx', 'css', 'html', 'py', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'hpp', 'bat', 'ps1', 'properties', 'service', 'timer', 'mount', 'rules', 'cron', 'plg', 'page']; + if (in_array($extension, $text, true)) { + return 'text'; + } + if ($extension === 'md') { + return 'markdown'; + } + if (in_array($extension, ['csv', 'tsv'], true)) { + return 'csv'; + } + if (in_array($extension, ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'], true)) { + return 'image'; + } + if ($extension === 'pdf') { + return 'pdf'; + } + if (in_array($extension, ['mp3', 'flac', 'm4a', 'ogg', 'wav'], true)) { + return 'audio'; + } + if (in_array($extension, ['mp4', 'mkv', 'webm', 'mov'], true)) { + return 'video'; + } + return null; +} + +function unav_preview_mime(string $extension, string $type): string { + $map = [ + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'webp' => 'image/webp', + 'svg' => 'image/svg+xml', + 'pdf' => 'application/pdf', + 'mp3' => 'audio/mpeg', + 'flac' => 'audio/flac', + 'm4a' => 'audio/mp4', + 'ogg' => 'audio/ogg', + 'wav' => 'audio/wav', + 'mp4' => 'video/mp4', + 'mkv' => 'video/x-matroska', + 'webm' => 'video/webm', + 'mov' => 'video/quicktime', + 'csv' => 'text/csv; charset=utf-8', + 'tsv' => 'text/tab-separated-values; charset=utf-8', + 'md' => 'text/markdown; charset=utf-8', + ]; + return $map[$extension] ?? ($type === 'text' ? 'text/plain; charset=utf-8' : 'application/octet-stream'); +} diff --git a/server/public/app.js b/server/public/app.js index 534beec..cde2149 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -40,7 +40,8 @@ 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']); +const previewExtensions = new Set(['txt', 'log', 'md', 'rd', 'rst', 'yaml', 'yml', 'json', 'xml', 'csv', 'tsv', 'ini', 'cfg', 'conf', 'env', 'toml', 'sql', 'nfo', 'srt', 'sub', 'vtt', 'm3u', 'm3u8', 'pls', 'sh', 'bash', 'zsh', 'php', 'js', 'ts', 'tsx', 'jsx', 'css', 'html', 'py', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'hpp', 'bat', 'ps1', 'properties', 'service', 'timer', 'mount', 'rules', 'cron', 'plg', 'page', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'pdf', 'mp3', 'flac', 'm4a', 'ogg', 'wav', 'mp4', 'mkv', 'webm', 'mov']); +const previewFilenames = new Set(['dockerfile', 'makefile']); document.querySelector('#newExplorerButton').addEventListener('click', () => openExplorer()); document.querySelector('#newPropertiesButton').addEventListener('click', () => openProperties()); @@ -429,10 +430,32 @@ function renderPreview(body, win) { ${escapeHtml(entry?.name || data.name || 'Vorschau')} ${escapeHtml(formatBytes(data.size || entry?.size || 0))} -
${escapeHtml(data.content || '')}
+ ${renderPreviewContent(data)}
`;
}
+function renderPreviewContent(data) {
+ if (data.type === 'markdown') {
+ return `${escapeHtml(data.content || '')}`;
+}
+
async function loadPreview(win, entry) {
win.data.loading = true;
win.data.error = null;
@@ -442,6 +465,9 @@ async function loadPreview(win, entry) {
win.data.entry = entry;
win.data.name = result.name;
win.data.size = result.size;
+ win.data.type = result.type;
+ win.data.extension = result.extension;
+ win.data.rawUrl = result.rawUrl;
win.data.content = result.content;
} catch (error) {
win.data.error = error.message;
@@ -451,6 +477,99 @@ async function loadPreview(win, entry) {
}
}
+function renderMarkdown(content) {
+ const blocks = [];
+ let listItems = [];
+ const flushList = () => {
+ if (listItems.length) {
+ blocks.push(`${renderInlineMarkdown(line)}
`); + } + flushList(); + return blocks.join(''); +} + +function renderInlineMarkdown(value) { + return escapeHtml(value) + .replace(/`([^`]+)`/g, '$1')
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
+ .replace(/\*([^*]+)\*/g, '$1');
+}
+
+function renderCsvPreview(content, delimiter) {
+ const rows = parseDelimitedRows(content, delimiter).slice(0, 80);
+ if (!rows.length) {
+ return 'Keine Tabellenzeilen.
'; + } + return ` +| ${escapeHtml(cell)} | `).join('')}