From ae5e192eac5e626c4d204de8a4d03bf95fe8cc4e Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:21:49 +0200 Subject: [PATCH] Embed U-Navigator in Dynamix --- README.md | 22 ++- build-plugin.sh | 11 +- packages/u-navigator-0.1.0.tgz | Bin 12914 -> 0 bytes packages/u-navigator-0.2.0.tgz | Bin 0 -> 9334 bytes .../plugins/u-navigator/U-Navigator.page | 47 +++---- .../emhttp/plugins/u-navigator/api/common.php | 132 ++++++++++++++++++ .../emhttp/plugins/u-navigator/api/config.php | 7 + .../emhttp/plugins/u-navigator/api/job.php | 44 ++++++ .../emhttp/plugins/u-navigator/api/list.php | 34 +++++ .../emhttp/plugins/u-navigator/api/upload.php | 29 ++++ .../u-navigator/scripts/rc.u-navigator | 62 -------- public/app.js | 15 +- public/styles.css | 132 ++++++++---------- u-navigator.plg | 18 +-- u-navigator.plg.in | 14 +- 15 files changed, 353 insertions(+), 214 deletions(-) delete mode 100644 packages/u-navigator-0.1.0.tgz create mode 100644 packages/u-navigator-0.2.0.tgz create mode 100644 plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php create mode 100644 plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php create mode 100644 plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php create mode 100644 plugin-root/usr/local/emhttp/plugins/u-navigator/api/list.php create mode 100644 plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php delete mode 100644 plugin-root/usr/local/emhttp/plugins/u-navigator/scripts/rc.u-navigator diff --git a/README.md b/README.md index ccad001..af55487 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Aktueller Stand: Kurzentscheidung: -U-Navigator ist als Unraid-Plugin machbar. Empfohlen wird ein hybrider Aufbau aus Unraid-Plugin, lokalem Backend-Dienst und moderner Weboberflaeche. +U-Navigator ist als natives Unraid-Dynamix-Plugin umgesetzt. Die Weboberflaeche wird direkt im Unraid-WebGUI geladen; die API-Endpunkte laufen als PHP-Dateien im Plugin. Naechster sinnvoller Schritt: -Go-Backend oder gebuendelte Runtime, damit das Plugin nicht mehr von einer vorhandenen Node.js-Installation abhaengt. +Robuste Hintergrundjobs fuer grosse Copy/Move-Operationen und eine Konfigurationsseite fuer erlaubte Roots. ## Unraid Plugin installieren @@ -37,23 +37,17 @@ Oder im WebGUI: Plugins -> Install Plugin -> URL einfuegen -> Install ``` -Danach starten: +Danach oeffnen: -```sh -rc.u-navigator start +```text +Tools -> U-Navigator ``` -Status pruefen: +Es wird kein extra Webserver und kein Node.js auf Unraid benoetigt. -```sh -rc.u-navigator status -``` +## Lokal testen -Wichtig: Dieser erste Plugin-Spike benoetigt `node` auf dem Unraid-System. Falls `rc.u-navigator start` meldet, dass Node.js fehlt, muss als naechster Schritt das Backend als Go-Binary gebaut oder Node.js separat bereitgestellt werden. - -## Lokal starten - -Der Prototyp nutzt standardmaessig `/mnt/user`. Auf Entwicklungsmaschinen ohne diesen Pfad kann ein Root gesetzt werden: +Der historische Node-Prototyp ist fuer lokale API-Tests noch im Repo. Auf Entwicklungsmaschinen ohne `/mnt/user` kann ein Root gesetzt werden: ```sh U_NAV_ROOT="$PWD" PORT=8088 npm start diff --git a/build-plugin.sh b/build-plugin.sh index 06004de..e6ad079 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,20 +2,17 @@ set -eu PLUGIN="u-navigator" -VERSION="0.1.0" +VERSION="0.2.0" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" rm -rf "$WORK_DIR" -mkdir -p "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/app" "$PKG_DIR" +mkdir -p "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets" "$PKG_DIR" cp -R plugin-root/usr "$WORK_DIR/" -cp package.json "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/app/" -cp -R public "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/app/" -cp -R server "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/app/" -find "$WORK_DIR" -name "*.test.js" -delete -chmod +x "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/scripts/rc.u-navigator" +cp public/app.js "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/app.js" +cp public/styles.css "$WORK_DIR/usr/local/emhttp/plugins/${PLUGIN}/assets/styles.css" tar -czf "$PKG_DIR/$PKG_NAME" -C "$WORK_DIR" usr rm -rf "$WORK_DIR" diff --git a/packages/u-navigator-0.1.0.tgz b/packages/u-navigator-0.1.0.tgz deleted file mode 100644 index 963e76d915455e950365367c00fcc3c5b953ec60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12914 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 literal 0 HcmV?d00001 diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page index c4293d0..f87382c 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/U-Navigator.page @@ -3,37 +3,24 @@ Title="U-Navigator" Icon="folder-open" --- - -
-

- U-Navigator laeuft als lokaler Dienst auf Port . - Falls der Rahmen leer bleibt, pruefe den Dienst mit rc.u-navigator status. -

- + +
+
+
+

U-Navigator

+

Initialisiere...

+
+ +
+
+ diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php new file mode 100644 index 0000000..1a78d5e --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php @@ -0,0 +1,132 @@ + 'root' . (count($resolved) + 1), + 'label' => $root, + 'path' => $real, + ]; + } + } + + if (!$resolved) { + unav_error(500, 'No usable U-Navigator root found'); + } + return $resolved; +} + +function unav_read_only(): bool { + return getenv('U_NAV_READ_ONLY') === '1'; +} + +function unav_json($payload, int $status = 200): void { + http_response_code($status); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode($payload, JSON_UNESCAPED_SLASHES); + exit; +} + +function unav_error(int $status, string $message): void { + unav_json(['error' => $message], $status); +} + +function unav_is_inside(string $path, string $root): bool { + if ($path === $root) { + return true; + } + return str_starts_with($path, rtrim($root, '/') . '/'); +} + +function unav_root_for_real(string $real): array { + foreach (unav_roots() as $root) { + if (unav_is_inside($real, $root['path'])) { + return $root; + } + } + unav_error(403, 'Path is outside allowed roots'); +} + +function unav_existing_path(?string $input): array { + $roots = unav_roots(); + $candidate = $input ?: $roots[0]['path']; + $real = realpath($candidate); + if ($real === false) { + unav_error(404, 'Path not found'); + } + $root = unav_root_for_real($real); + return ['root' => $root, 'path' => $real]; +} + +function unav_destination_path(?string $input): array { + if (!$input) { + unav_error(400, 'Missing destination'); + } + $candidate = $input; + $parent = dirname($candidate); + $parentReal = realpath($parent); + if ($parentReal === false) { + unav_error(404, 'Destination parent not found'); + } + $root = unav_root_for_real($parentReal); + return ['root' => $root, 'path' => $parentReal . '/' . basename($candidate)]; +} + +function unav_assert_writable(): void { + if (unav_read_only()) { + unav_error(403, 'Read-only mode is enabled'); + } +} + +function unav_request_json(): array { + $raw = file_get_contents('php://input'); + $data = json_decode($raw ?: '{}', true); + if (!is_array($data)) { + unav_error(400, 'Invalid JSON body'); + } + return $data; +} + +function unav_copy_recursive(string $source, string $destination): void { + if (is_link($source)) { + unav_error(400, 'Copying symlinks is not allowed'); + } + if (is_dir($source)) { + if (!mkdir($destination, 0777, true) && !is_dir($destination)) { + unav_error(500, 'Could not create destination directory'); + } + foreach (scandir($source) ?: [] as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + unav_copy_recursive($source . '/' . $entry, $destination . '/' . $entry); + } + return; + } + if (!is_file($source)) { + unav_error(400, 'Only files and directories can be copied'); + } + if (!copy($source, $destination)) { + unav_error(500, 'Copy failed'); + } +} + +function unav_guard_destination(string $source, string $destination): void { + if (file_exists($destination)) { + unav_error(409, 'Destination already exists'); + } + if ($source === $destination || str_starts_with($destination, rtrim($source, '/') . '/')) { + unav_error(400, 'Destination cannot be inside source'); + } +} diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php new file mode 100644 index 0000000..9bab61b --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php @@ -0,0 +1,7 @@ + unav_read_only(), + 'roots' => unav_roots(), +]); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php new file mode 100644 index 0000000..5915c84 --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/job.php @@ -0,0 +1,44 @@ + $_GET['id'] ?? '0', + 'type' => 'unknown', + 'status' => 'completed', + 'progress' => 100, + 'message' => 'Completed', + 'error' => null, + 'createdAt' => date(DATE_ATOM), + 'updatedAt' => date(DATE_ATOM), + ]); +} + +$data = unav_request_json(); +$source = unav_existing_path($data['source'] ?? null); +$destination = unav_destination_path($data['destination'] ?? null); +unav_guard_destination($source['path'], $destination['path']); + +if ($action === 'move') { + if (!rename($source['path'], $destination['path'])) { + unav_error(500, 'Move failed'); + } +} elseif ($action === 'copy') { + unav_copy_recursive($source['path'], $destination['path']); +} else { + unav_error(404, 'Unknown job action'); +} + +unav_json([ + 'id' => (string)time(), + 'type' => $action, + 'status' => 'completed', + 'progress' => 100, + 'message' => 'Completed', + 'error' => null, + 'createdAt' => date(DATE_ATOM), + 'updatedAt' => date(DATE_ATOM), +], 202); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/list.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/list.php new file mode 100644 index 0000000..c1b9180 --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/list.php @@ -0,0 +1,34 @@ + $name, + 'path' => $full, + 'type' => $type, + 'size' => is_file($full) ? filesize($full) : 0, + 'modifiedAt' => date(DATE_ATOM, filemtime($full) ?: time()), + ]; +} + +usort($entries, function ($a, $b) { + if ($a['type'] === 'directory' && $b['type'] !== 'directory') return -1; + if ($a['type'] !== 'directory' && $b['type'] === 'directory') return 1; + return strnatcasecmp($a['name'], $b['name']); +}); + +unav_json([ + 'path' => $resolved['path'], + 'entries' => $entries, +]); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php new file mode 100644 index 0000000..f22f4e5 --- /dev/null +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/upload.php @@ -0,0 +1,29 @@ + $name) { + if ($_FILES['files']['error'][$index] !== UPLOAD_ERR_OK) { + unav_error(400, 'Upload failed'); + } + $safeName = basename($name); + $destination = unav_destination_path($target['path'] . '/' . $safeName); + if (file_exists($destination['path'])) { + unav_error(409, 'Destination already exists'); + } + if (!move_uploaded_file($_FILES['files']['tmp_name'][$index], $destination['path'])) { + unav_error(500, 'Could not store uploaded file'); + } + $uploaded[] = [ + 'name' => $safeName, + 'size' => filesize($destination['path']) ?: 0, + ]; +} + +unav_json(['uploaded' => $uploaded], 201); diff --git a/plugin-root/usr/local/emhttp/plugins/u-navigator/scripts/rc.u-navigator b/plugin-root/usr/local/emhttp/plugins/u-navigator/scripts/rc.u-navigator deleted file mode 100644 index 91996bd..0000000 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/scripts/rc.u-navigator +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash - -PLUGIN="u-navigator" -APP_DIR="/usr/local/emhttp/plugins/${PLUGIN}/app" -PID_FILE="/var/run/${PLUGIN}.pid" -LOG_FILE="/var/log/${PLUGIN}.log" -PORT="${U_NAV_PORT:-8088}" -ROOTS="${U_NAV_ROOTS:-/mnt/user}" -HOST="${U_NAV_HOST:-0.0.0.0}" - -is_running() { - [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null -} - -start() { - if is_running; then - echo "${PLUGIN} is already running with PID $(cat "$PID_FILE")" - return 0 - fi - - if ! command -v node >/dev/null 2>&1; then - echo "node is required but was not found. Install/provide Node.js before starting ${PLUGIN}." - return 1 - fi - - mkdir -p "$(dirname "$PID_FILE")" - U_NAV_ROOTS="$ROOTS" U_NAV_HOST="$HOST" PORT="$PORT" nohup node "${APP_DIR}/server/server.js" >>"$LOG_FILE" 2>&1 & - echo $! > "$PID_FILE" - echo "${PLUGIN} started on ${HOST}:${PORT} with roots ${ROOTS}" -} - -stop() { - if ! is_running; then - rm -f "$PID_FILE" - echo "${PLUGIN} is not running" - return 0 - fi - - kill "$(cat "$PID_FILE")" 2>/dev/null || true - rm -f "$PID_FILE" - echo "${PLUGIN} stopped" -} - -status() { - if is_running; then - echo "${PLUGIN} is running with PID $(cat "$PID_FILE")" - else - echo "${PLUGIN} is not running" - return 1 - fi -} - -case "$1" in - start) start ;; - stop) stop ;; - restart) stop; start ;; - status) status ;; - *) - echo "Usage: $0 {start|stop|restart|status}" - exit 2 - ;; -esac diff --git a/public/app.js b/public/app.js index 20173c1..519cec2 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,6 @@ const workspace = document.querySelector('#workspace'); const statusText = document.querySelector('#statusText'); +const API_BASE = window.UNAV_API_BASE || '/api'; const state = { config: null, windows: [], @@ -25,7 +26,7 @@ init(); async function init() { try { - state.config = await api('/api/config'); + state.config = await api(apiUrl('config')); statusText.textContent = `${state.config.roots.length} Root, ${state.config.readOnly ? 'Read-only' : 'Schreibzugriff aktiv'}`; openExplorer(state.config.roots[0].path); openProperties(); @@ -276,7 +277,7 @@ async function loadExplorer(win, targetPath) { win.data.path = targetPath; render(); try { - const result = await api(`/api/list?path=${encodeURIComponent(targetPath)}`); + const result = await api(apiUrl(`list.php?path=${encodeURIComponent(targetPath)}`)); win.data.path = result.path; win.data.entries = result.entries; } catch (error) { @@ -307,7 +308,7 @@ async function handleDrop(event, win, targetPath) { for (const file of event.dataTransfer.files) { form.append('files', file, file.name); } - await fetchChecked(`/api/upload?path=${encodeURIComponent(targetPath)}`, { + await fetchChecked(apiUrl(`upload.php?path=${encodeURIComponent(targetPath)}`), { method: 'POST', body: form }); @@ -316,7 +317,7 @@ async function handleDrop(event, win, targetPath) { } async function createJob(type, source, destination) { - const job = await api(`/api/jobs/${type}`, { + const job = await api(apiUrl(`job.php?action=${encodeURIComponent(type)}`), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source, destination }) @@ -327,7 +328,7 @@ async function createJob(type, source, destination) { } async function pollJob(id) { - const job = await api(`/api/jobs/${id}`); + const job = await api(apiUrl(`job.php?id=${encodeURIComponent(id)}`)); state.jobs.set(id, job); render(); if (['queued', 'running', 'cancel_requested'].includes(job.status)) { @@ -446,3 +447,7 @@ function escapeAttr(value) { function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); } + +function apiUrl(path) { + return `${API_BASE.replace(/\/$/, '')}/${path.replace(/^\//, '')}`; +} diff --git a/public/styles.css b/public/styles.css index e60dfd4..758e203 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,5 +1,4 @@ -:root { - color-scheme: light dark; +.u-nav { --bg: #f4f6f8; --surface: #ffffff; --surface-2: #eef2f5; @@ -8,12 +7,17 @@ --line: #cfd8e1; --accent: #e86f2d; --accent-2: #287a65; - --danger: #b52c34; --shadow: 0 18px 55px rgba(24, 34, 45, 0.22); + background: var(--bg); + color: var(--text); + display: grid; + font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + grid-template-rows: auto 1fr; + min-height: calc(100vh - 170px); } @media (prefers-color-scheme: dark) { - :root { + .u-nav { --bg: #101418; --surface: #1b2229; --surface-2: #242e37; @@ -24,22 +28,16 @@ } } -* { +.u-nav *, +.u-nav *::before, +.u-nav *::after { box-sizing: border-box; } -body { - margin: 0; - min-height: 100vh; - background: var(--bg); - color: var(--text); - font: 14px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -} - -button { +.u-nav button { + background: var(--surface); border: 1px solid var(--line); border-radius: 6px; - background: var(--surface); color: var(--text); cursor: pointer; font: inherit; @@ -47,17 +45,11 @@ button { padding: 0 10px; } -button:hover { +.u-nav button:hover { border-color: var(--accent); } -button.primary { - background: var(--accent); - border-color: var(--accent); - color: #fff; -} - -button.icon { +.u-nav button.icon { align-items: center; display: inline-flex; height: 30px; @@ -67,13 +59,7 @@ button.icon { width: 32px; } -.app-shell { - display: grid; - grid-template-rows: auto 1fr; - min-height: 100vh; -} - -.topbar { +.u-nav .topbar { align-items: center; background: var(--surface); border-bottom: 1px solid var(--line); @@ -83,30 +69,32 @@ button.icon { padding: 12px 16px; } -.topbar h1 { +.u-nav .topbar h1 { + color: var(--text); font-size: 18px; line-height: 1; margin: 0 0 4px; } -.topbar p { +.u-nav .topbar p, +.u-nav .muted { color: var(--muted); margin: 0; } -.toolbar { +.u-nav .toolbar { display: flex; flex-wrap: wrap; gap: 8px; } -.workspace { +.u-nav .workspace { min-height: 0; overflow: hidden; position: relative; } -.window { +.u-nav .window { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; @@ -119,15 +107,15 @@ button.icon { position: absolute; } -.window.focused { +.u-nav .window.focused { border-color: color-mix(in srgb, var(--accent) 62%, var(--line)); } -.window.maximized { +.u-nav .window.maximized { border-radius: 0; } -.titlebar { +.u-nav .titlebar { align-items: center; background: var(--surface-2); border-bottom: 1px solid var(--line); @@ -140,24 +128,24 @@ button.icon { user-select: none; } -.titlebar strong { +.u-nav .titlebar strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.window-actions { +.u-nav .window-actions { display: flex; gap: 6px; } -.window-body { +.u-nav .window-body { min-height: 0; overflow: auto; padding: 10px; } -.resize-handle { +.u-nav .resize-handle { bottom: 0; cursor: nwse-resize; height: 18px; @@ -166,7 +154,7 @@ button.icon { width: 18px; } -.resize-handle::after { +.u-nav .resize-handle::after { border-bottom: 2px solid var(--muted); border-right: 2px solid var(--muted); bottom: 5px; @@ -177,7 +165,7 @@ button.icon { width: 8px; } -.pathbar { +.u-nav .pathbar { align-items: center; display: grid; gap: 8px; @@ -185,7 +173,7 @@ button.icon { margin-bottom: 10px; } -.pathbar input { +.u-nav .pathbar input { background: var(--surface-2); border: 1px solid var(--line); border-radius: 6px; @@ -196,53 +184,53 @@ button.icon { width: 100%; } -.drop-zone { +.u-nav .drop-zone { border: 1px dashed transparent; border-radius: 8px; min-height: 180px; } -.drop-zone.drag-over { +.u-nav .drop-zone.drag-over { background: color-mix(in srgb, var(--accent) 10%, transparent); border-color: var(--accent); } -.file-table { +.u-nav .file-table { border-collapse: collapse; width: 100%; } -.file-table th, -.file-table td { +.u-nav .file-table th, +.u-nav .file-table td { border-bottom: 1px solid var(--line); padding: 8px 7px; text-align: left; vertical-align: middle; } -.file-table th { +.u-nav .file-table th { color: var(--muted); font-size: 12px; font-weight: 600; } -.file-row { +.u-nav .file-row { cursor: default; } -.file-row:hover, -.file-row.selected { +.u-nav .file-row:hover, +.u-nav .file-row.selected { background: var(--surface-2); } -.name-cell { +.u-nav .name-cell { align-items: center; display: flex; gap: 8px; min-width: 150px; } -.badge { +.u-nav .badge { border: 1px solid var(--line); border-radius: 999px; color: var(--muted); @@ -253,22 +241,18 @@ button.icon { padding: 2px 8px; } -.muted { - color: var(--muted); -} - -.queue-list { +.u-nav .queue-list { display: grid; gap: 10px; } -.job { +.u-nav .job { border: 1px solid var(--line); border-radius: 8px; padding: 10px; } -.progress { +.u-nav .progress { background: var(--surface-2); border-radius: 999px; height: 8px; @@ -276,40 +260,40 @@ button.icon { overflow: hidden; } -.progress div { +.u-nav .progress div { background: var(--accent-2); height: 100%; width: 0; } -.properties dl { +.u-nav .properties dl { display: grid; - grid-template-columns: max-content 1fr; gap: 8px 14px; + grid-template-columns: max-content 1fr; margin: 0; } -.properties dt { +.u-nav .properties dt { color: var(--muted); } -.properties dd { +.u-nav .properties dd { margin: 0; overflow-wrap: anywhere; } @media (max-width: 720px) { - .topbar { + .u-nav .topbar { align-items: flex-start; flex-direction: column; } - .workspace { + .u-nav .workspace { overflow: auto; } - .window, - .window.maximized { + .u-nav .window, + .u-nav .window.maximized { border-left: 0; border-radius: 0; border-right: 0; @@ -321,11 +305,11 @@ button.icon { width: 100% !important; } - .window:not(.focused) { + .u-nav .window:not(.focused) { display: none; } - .resize-handle { + .u-nav .resize-handle { display: none; } } diff --git a/u-navigator.plg b/u-navigator.plg index 0a71860..af51a8c 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,37 +1,32 @@ - + ]> ### &version; -- Initial U-Navigator plugin spike. -- Installs WebGUI page, Node.js prototype backend, in-app window manager and drag-and-drop file navigator. +- Native Dynamix-embedded U-Navigator page. +- Adds PHP API endpoints for listing, upload, move and copy without a separate web server. https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package; -9d6e248cb70c803f04bf491b06520764 +47fea71317002e9ea131ca468a870c69 mkdir -p /boot/config/plugins/&name; tar -xzf /boot/config/plugins/&name;/&package; -C / -chmod +x /usr/local/emhttp/plugins/&name;/scripts/rc.u-navigator -ln -sf /usr/local/emhttp/plugins/&name;/scripts/rc.u-navigator /usr/local/sbin/rc.u-navigator +find /boot/config/plugins/&name; -type f -name "&name;-*.tgz" ! -name "&package;" -delete echo "" echo "-----------------------------------------------------------" echo " &name; &version; has been installed." echo "" -echo " Start: rc.u-navigator start" -echo " Stop: rc.u-navigator stop" -echo " Status: rc.u-navigator status" -echo "" -echo " Note: This prototype requires Node.js to be available on Unraid." +echo " Open: Tools -> U-Navigator" echo "-----------------------------------------------------------" echo "" @@ -39,7 +34,6 @@ echo "" -rc.u-navigator stop 2>/dev/null || true rm -f /usr/local/sbin/rc.u-navigator rm -rf /usr/local/emhttp/plugins/&name; rm -rf /boot/config/plugins/&name; diff --git a/u-navigator.plg.in b/u-navigator.plg.in index bf3dce8..cbffe0e 100644 --- a/u-navigator.plg.in +++ b/u-navigator.plg.in @@ -8,8 +8,8 @@ ### &version; -- Initial U-Navigator plugin spike. -- Installs WebGUI page, Node.js prototype backend, in-app window manager and drag-and-drop file navigator. +- Native Dynamix-embedded U-Navigator page. +- Adds PHP API endpoints for listing, upload, move and copy without a separate web server. @@ -21,17 +21,12 @@ mkdir -p /boot/config/plugins/&name; tar -xzf /boot/config/plugins/&name;/&package; -C / -chmod +x /usr/local/emhttp/plugins/&name;/scripts/rc.u-navigator -ln -sf /usr/local/emhttp/plugins/&name;/scripts/rc.u-navigator /usr/local/sbin/rc.u-navigator +find /boot/config/plugins/&name; -type f -name "&name;-*.tgz" ! -name "&package;" -delete echo "" echo "-----------------------------------------------------------" echo " &name; &version; has been installed." echo "" -echo " Start: rc.u-navigator start" -echo " Stop: rc.u-navigator stop" -echo " Status: rc.u-navigator status" -echo "" -echo " Note: This prototype requires Node.js to be available on Unraid." +echo " Open: Tools -> U-Navigator" echo "-----------------------------------------------------------" echo "" @@ -39,7 +34,6 @@ echo "" -rc.u-navigator stop 2>/dev/null || true rm -f /usr/local/sbin/rc.u-navigator rm -rf /usr/local/emhttp/plugins/&name; rm -rf /boot/config/plugins/&name;