diff --git a/build-plugin.sh b/build-plugin.sh index b98b2b9..b1ad44c 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r026" +VERSION="2026.06.23.r027" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r026.tgz b/packages/u-navigator-2026.06.23.r026.tgz deleted file mode 100644 index 33ab2c0..0000000 Binary files a/packages/u-navigator-2026.06.23.r026.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r027.tgz b/packages/u-navigator-2026.06.23.r027.tgz new file mode 100644 index 0000000..ace2b04 Binary files /dev/null and b/packages/u-navigator-2026.06.23.r027.tgz differ 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 a71a65b..a533eeb 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 @@ -17,6 +17,7 @@ $version = "__VERSION__"; +
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 index d7bb740..7661240 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/common.php @@ -2,6 +2,39 @@ const UNAV_DEFAULT_ROOT = '/mnt/user'; const UNAV_JOB_DIR = '/tmp/u-navigator/jobs'; +const UNAV_SETTINGS_FILE = '/boot/config/plugins/u-navigator/settings.json'; + +function unav_settings(): array { + $defaults = [ + 'readOnly' => true, + 'debug' => false, + ]; + if (!is_file(UNAV_SETTINGS_FILE)) { + return $defaults; + } + $settings = json_decode(file_get_contents(UNAV_SETTINGS_FILE) ?: '{}', true); + if (!is_array($settings)) { + return $defaults; + } + return array_merge($defaults, [ + 'readOnly' => filter_var($settings['readOnly'] ?? $defaults['readOnly'], FILTER_VALIDATE_BOOLEAN), + 'debug' => filter_var($settings['debug'] ?? $defaults['debug'], FILTER_VALIDATE_BOOLEAN), + ]); +} + +function unav_settings_write(array $settings): array { + $current = unav_settings(); + $next = [ + 'readOnly' => filter_var($settings['readOnly'] ?? $current['readOnly'], FILTER_VALIDATE_BOOLEAN), + 'debug' => filter_var($settings['debug'] ?? $current['debug'], FILTER_VALIDATE_BOOLEAN), + ]; + $dir = dirname(UNAV_SETTINGS_FILE); + if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { + unav_error(500, 'Could not create settings directory'); + } + file_put_contents(UNAV_SETTINGS_FILE, json_encode($next, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT), LOCK_EX); + return $next; +} function unav_roots(): array { $configured = getenv('U_NAV_ROOTS') ?: UNAV_DEFAULT_ROOT; @@ -29,7 +62,14 @@ function unav_roots(): array { } function unav_read_only(): bool { - return getenv('U_NAV_READ_ONLY') === '1'; + $env = getenv('U_NAV_READ_ONLY'); + if ($env === '1') { + return true; + } + if ($env === '0') { + return false; + } + return unav_settings()['readOnly']; } function unav_function_available(string $name): bool { 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 index 9bab61b..d645605 100644 --- a/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php +++ b/plugin-root/usr/local/emhttp/plugins/u-navigator/api/config.php @@ -1,7 +1,19 @@ $settings['readOnly'], + 'debug' => $settings['debug'], + 'roots' => unav_roots(), + ]); +} + +$settings = unav_settings(); unav_json([ 'readOnly' => unav_read_only(), + 'debug' => $settings['debug'], 'roots' => unav_roots(), ]); diff --git a/server/public/app.js b/server/public/app.js index 7b21e9d..1ab5453 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -2,6 +2,7 @@ const workspace = document.querySelector('#workspace'); const appShell = document.querySelector('.u-nav'); const statusText = document.querySelector('#statusText'); const transferButton = document.querySelector('#newQueueButton'); +const settingsButton = document.querySelector('#settingsButton'); const API_BASE = window.UNAV_API_BASE || '/api'; const APP_VERSION = window.UNAV_VERSION || 'dev'; const state = { @@ -23,6 +24,7 @@ const state = { transferPanelVisible: false, transferPanelClosing: false, transferAutoHideTimer: null, + settingsPanelVisible: false, nextWindowId: 1, zIndex: 20 }; @@ -37,6 +39,7 @@ const icons = { document.querySelector('#newExplorerButton').addEventListener('click', () => openExplorer()); document.querySelector('#newPropertiesButton').addEventListener('click', () => openProperties()); transferButton.addEventListener('click', () => toggleTransferPanel()); +settingsButton.addEventListener('click', () => toggleSettingsPanel()); window.addEventListener('error', (event) => { recordDebug('window.error', { message: event.message, @@ -58,7 +61,7 @@ async function init() { if (!state.config.roots?.length) { throw new Error('No U-Navigator roots configured'); } - statusText.textContent = `${state.config.roots.length} Root, ${state.config.readOnly ? 'Read-only' : 'Schreibzugriff aktiv'} · ${APP_VERSION}`; + updateStatusText(); openExplorer(state.config.roots[0].path); } catch (error) { statusText.textContent = error.message; @@ -143,6 +146,7 @@ function render() { workspace.appendChild(renderContextMenu()); } renderTransferPanel(); + renderSettingsPanel(); } function renderWindow(win) { @@ -213,11 +217,11 @@ function renderExplorer(body, win) {
- - + +
- - + +
@@ -378,6 +382,15 @@ function queueMarkup() {
`).join('') : '

Keine Transfers.

'; + const debugSection = state.config?.debug ? debugMarkup() : ''; + + return ` +
${jobMarkup}
+ ${debugSection} + `; +} + +function debugMarkup() { const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
#${item.id} ${escapeHtml(item.label)} @@ -387,7 +400,6 @@ function queueMarkup() { `).join('') : '

Noch keine Debug-Einträge.

'; return ` -
${jobMarkup}
Debug @@ -495,6 +507,100 @@ function renderTransferPanel() { transferButton.setAttribute('aria-expanded', 'true'); } +function toggleSettingsPanel() { + state.settingsPanelVisible = !state.settingsPanelVisible; + renderSettingsPanel(); +} + +function renderSettingsPanel() { + const existing = appShell.querySelector('.settings-panel'); + if (!state.settingsPanelVisible) { + existing?.remove(); + settingsButton.classList.remove('active'); + settingsButton.setAttribute('aria-expanded', 'false'); + return; + } + + const panel = existing || document.createElement('aside'); + panel.className = 'settings-panel'; + panel.setAttribute('aria-label', 'Einstellungen'); + panel.innerHTML = ` +
+ Einstellungen + +
+
+ + +
+ `; + if (!existing) { + appShell.appendChild(panel); + } + + const buttonRect = settingsButton.getBoundingClientRect(); + const shellRect = appShell.getBoundingClientRect(); + panel.style.right = `${Math.max(16, shellRect.right - buttonRect.right)}px`; + panel.style.top = `${buttonRect.bottom - shellRect.top + 8}px`; + + panel.querySelector('[data-action="close-settings"]').addEventListener('click', () => { + state.settingsPanelVisible = false; + renderSettingsPanel(); + }); + panel.querySelector('[data-setting="debug"]').addEventListener('change', (event) => { + saveSettings({ debug: event.currentTarget.checked }); + }); + panel.querySelector('[data-setting="write"]').addEventListener('change', (event) => { + saveSettings({ readOnly: !event.currentTarget.checked }); + }); + settingsButton.classList.add('active'); + settingsButton.setAttribute('aria-expanded', 'true'); +} + +async function saveSettings(changes) { + const body = new URLSearchParams(); + const readOnly = changes.readOnly ?? state.config.readOnly; + const debug = changes.debug ?? state.config.debug; + body.set('readOnly', readOnly ? '1' : '0'); + body.set('debug', debug ? '1' : '0'); + appendCsrf(body); + try { + state.config = await api(apiUrl('config.php'), { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body + }); + updateStatusText(); + if (!state.config.debug) { + state.debug = []; + } + render(); + } catch (error) { + showError(error); + } +} + +function updateStatusText() { + if (!state.config) return; + statusText.textContent = `${state.config.roots.length} Root, ${state.config.readOnly ? 'Read-only' : 'Schreibzugriff aktiv'} · Debug ${state.config.debug ? 'aktiv' : 'aus'} · ${APP_VERSION}`; +} + +function isReadOnly() { + return Boolean(state.config?.readOnly); +} + async function loadExplorer(win, targetPath) { win.data.loading = true; win.data.error = null; @@ -539,6 +645,10 @@ async function handleDrop(event, win, targetPath) { } async function createJob(type, source, destination, refreshWindowIds = []) { + if (['move', 'copy'].includes(type) && isReadOnly()) { + showError(new Error('Read-only mode ist aktiv.')); + return null; + } recordDebug('job.create.request', { type, source, destination, refreshWindowIds }); const body = new URLSearchParams(); body.set('source', source); @@ -631,6 +741,10 @@ function setDropEffect(event, effect) { } async function renameEntry(win, entry) { + if (isReadOnly()) { + showError(new Error('Read-only mode ist aktiv.')); + return; + } const nextName = prompt('Neuer Name', entry.name); if (!nextName || nextName === entry.name) { return; @@ -643,6 +757,10 @@ async function renameEntry(win, entry) { } async function promptTransfer(win, entry, type) { + if (isReadOnly()) { + showError(new Error('Read-only mode ist aktiv.')); + return; + } const targetDir = prompt(type === 'copy' ? 'Kopieren nach Ordner' : 'Verschieben nach Ordner', win.data.path); if (!targetDir) { return; @@ -681,9 +799,11 @@ function renderContextMenu() { actions.push(['Öffnen', () => loadExplorer(win, entry.path)]); } actions.push(['Eigenschaften', () => showPropertiesForEntry(win, entry)]); - actions.push(['Umbenennen', () => renameEntry(win, entry)]); - actions.push(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]); - actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]); + if (!isReadOnly()) { + actions.push(['Umbenennen', () => renameEntry(win, entry)]); + actions.push(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]); + actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]); + } } else { actions.push(['Neu laden', () => loadExplorer(win, win.data.path)]); } @@ -1234,6 +1354,9 @@ function getCsrfToken() { } function recordDebug(label, data = {}) { + if (!state.config?.debug) { + return null; + } const item = { id: state.nextDebugId++, time: new Date().toLocaleTimeString(), diff --git a/server/public/styles.css b/server/public/styles.css index e425c0d..620f78a 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -193,12 +193,12 @@ } .u-nav .pathbar .up-button { - flex: 0 0 38px; + flex: 0 0 32px; font-size: 18px; line-height: 1; } -.u-nav .pathbar > button:not(.icon), +.u-nav .pathbar > button.icon, .u-nav .pathbar .view-toggle { flex: 0 0 auto; } @@ -225,7 +225,7 @@ .u-nav .view-toggle button { border-radius: 0; flex: 0 0 auto; - min-width: 88px; + min-width: 32px; overflow: visible; } @@ -535,6 +535,61 @@ padding: 10px; } +.u-nav .settings-panel { + animation: transfer-slide-down 160ms ease-out both; + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: var(--shadow); + min-width: 320px; + overflow: hidden; + position: absolute; + transform-origin: top right; + width: min(380px, calc(100% - 32px)); + z-index: 10010; +} + +.u-nav .settings-panel-header { + align-items: center; + background: var(--surface-2); + border-bottom: 1px solid var(--line); + display: flex; + justify-content: space-between; + min-height: 40px; + padding: 5px 8px 5px 12px; +} + +.u-nav .settings-panel-body { + display: grid; + gap: 2px; + padding: 8px; +} + +.u-nav .setting-row { + align-items: center; + border-radius: 6px; + display: grid; + gap: 14px; + grid-template-columns: 1fr auto; + padding: 10px; +} + +.u-nav .setting-row:hover { + background: var(--surface-2); +} + +.u-nav .setting-row small { + color: var(--muted); + display: block; + margin-top: 2px; +} + +.u-nav .setting-row input { + accent-color: var(--accent); + height: 18px; + width: 18px; +} + @keyframes transfer-slide-down { from { opacity: 0; @@ -675,4 +730,11 @@ right: 10px !important; width: auto !important; } + + .u-nav .settings-panel { + left: 10px !important; + min-width: 0; + right: 10px !important; + width: auto !important; + } } diff --git a/u-navigator.plg b/u-navigator.plg index c17474e..d101dd9 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; -9af5add68cb11a97e1c88afae8621497 +145d13dcb4bf35ced2981b0e219cb3a0