diff --git a/build-plugin.sh b/build-plugin.sh index fe9af14..1b2133a 100755 --- a/build-plugin.sh +++ b/build-plugin.sh @@ -2,7 +2,7 @@ set -eu PLUGIN="u-navigator" -VERSION="2026.06.23.r038" +VERSION="2026.06.23.r039" PKG_DIR="packages" WORK_DIR=".plugin-build" PKG_NAME="${PLUGIN}-${VERSION}.tgz" diff --git a/packages/u-navigator-2026.06.23.r038.tgz b/packages/u-navigator-2026.06.23.r038.tgz deleted file mode 100644 index c5affce..0000000 Binary files a/packages/u-navigator-2026.06.23.r038.tgz and /dev/null differ diff --git a/packages/u-navigator-2026.06.23.r039.tgz b/packages/u-navigator-2026.06.23.r039.tgz new file mode 100644 index 0000000..24386ce Binary files /dev/null and b/packages/u-navigator-2026.06.23.r039.tgz differ diff --git a/server/public/app.js b/server/public/app.js index 7f8d51d..2c47456 100644 --- a/server/public/app.js +++ b/server/public/app.js @@ -11,6 +11,7 @@ const state = { focusedId: null, selectedEntry: null, contextMenu: null, + permissionsDialog: null, draggedEntry: null, pointerDrag: null, suppressClickPath: null, @@ -191,6 +192,9 @@ function render() { if (state.contextMenu) { workspace.appendChild(renderContextMenu()); } + if (state.permissionsDialog) { + workspace.appendChild(renderPermissionsDialog()); + } renderTransferPanel(); renderSettingsPanel(); } @@ -1012,32 +1016,124 @@ async function promptTransfer(win, entry, type) { await createJob(type, entry.path, `${targetDir.replace(/\/$/, '')}/${entry.name}`, []); } -async function promptPermissions(win, entry) { +function openPermissionsDialog(win, entry) { if (isReadOnly()) { showError(new Error('Read-only mode ist aktiv.')); return; } const entries = selectedPathSet(win).has(entry.path) ? selectedEntries(win) : [entry]; - const mode = prompt('Berechtigungen als Oktalmodus, z.B. 0644 oder 0755', entry.permissions ? `0${entry.permissions}`.slice(-4) : ''); - if (mode === null) return; - const owner = prompt('Besitzer ändern, leer lassen für unverändert', entry.owner || ''); - if (owner === null) return; - const group = prompt('Gruppe ändern, leer lassen für unverändert', entry.group || ''); - if (group === null) return; - const hasDirectory = entries.some((item) => item.type === 'directory'); - const recursive = hasDirectory && confirm('Rekursiv auf Unterordner und Dateien anwenden?'); + state.permissionsDialog = { + winId: win.id, + entries, + mode: entry.permissions ? `0${entry.permissions}`.slice(-4) : '0755', + owner: entry.owner || '', + group: entry.group || '', + recursive: false, + hasDirectory: entries.some((item) => item.type === 'directory') + }; + render(); +} + +function renderPermissionsDialog() { + const dialog = state.permissionsDialog; + const overlay = document.createElement('div'); + overlay.className = 'modal-overlay'; + overlay.innerHTML = ` +
+ `; + + const form = overlay.querySelector('form'); + form.addEventListener('click', (event) => event.stopPropagation()); + overlay.addEventListener('click', () => closePermissionsDialog()); + overlay.querySelectorAll('[data-action="cancel"]').forEach((button) => { + button.addEventListener('click', () => closePermissionsDialog()); + }); + for (const checkboxName of ['changeMode', 'changeOwner', 'changeGroup']) { + const checkbox = form.elements[checkboxName]; + const inputName = checkboxName.replace('change', '').toLowerCase(); + const input = form.elements[inputName]; + checkbox.addEventListener('change', () => { + input.disabled = !checkbox.checked; + if (!input.disabled) input.focus(); + }); + } + for (const button of overlay.querySelectorAll('[data-mode]')) { + button.addEventListener('click', () => { + form.elements.changeMode.checked = true; + form.elements.mode.disabled = false; + form.elements.mode.value = button.dataset.mode; + }); + } + form.addEventListener('submit', (event) => { + event.preventDefault(); + applyPermissionsDialog(form).catch(showError); + }); + return overlay; +} + +function closePermissionsDialog() { + state.permissionsDialog = null; + render(); +} + +async function applyPermissionsDialog(form) { + const dialog = state.permissionsDialog; + if (!dialog) return; const options = { - mode: mode.trim(), - owner: owner.trim(), - group: group.trim(), - recursive: recursive ? '1' : '0' + mode: form.elements.changeMode.checked ? form.elements.mode.value.trim() : '', + owner: form.elements.changeOwner.checked ? form.elements.owner.value.trim() : '', + group: form.elements.changeGroup.checked ? form.elements.group.value.trim() : '', + recursive: form.elements.recursive.checked ? '1' : '0' }; if (!options.mode && !options.owner && !options.group) { showError(new Error('Keine Änderung angegeben.')); return; } - for (const item of entries) { - await createJob('permissions', item.path, null, [win.id], options); + if (options.mode && !/^[0-7]{3,4}$/.test(options.mode)) { + showError(new Error('Berechtigungen müssen aus 3 oder 4 Oktalziffern bestehen.')); + return; + } + const win = state.windows.find((item) => item.id === dialog.winId); + closePermissionsDialog(); + for (const item of dialog.entries) { + await createJob('permissions', item.path, null, [win?.id], options); } } @@ -1076,7 +1172,7 @@ function renderContextMenu() { actions.push(['Eigenschaften', () => showPropertiesForEntry(win, entry)]); if (!isReadOnly()) { actions.push(['Umbenennen', () => renameEntry(win, entry)]); - actions.push(['Berechtigungen...', () => promptPermissions(win, entry)]); + actions.push(['Berechtigungen...', () => openPermissionsDialog(win, entry)]); actions.push(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]); actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]); } diff --git a/server/public/styles.css b/server/public/styles.css index 46ea6d0..6beb83c 100644 --- a/server/public/styles.css +++ b/server/public/styles.css @@ -51,6 +51,12 @@ border-color: var(--accent); } +.u-nav button.primary { + background: var(--accent); + border-color: var(--accent); + color: #ffffff; +} + .u-nav button.icon { align-items: center; display: inline-flex; @@ -496,6 +502,101 @@ border-color: var(--line); } +.u-nav .modal-overlay { + align-items: center; + background: rgba(10, 14, 18, 0.42); + display: flex; + inset: 0; + justify-content: center; + padding: 18px; + position: absolute; + z-index: 100020; +} + +.u-nav .permissions-modal { + background: var(--surface); + border: 1px solid var(--line); + border-radius: 8px; + box-shadow: var(--shadow); + color: var(--text); + display: grid; + max-height: min(640px, 100%); + max-width: 100%; + min-width: min(420px, 100%); + overflow: hidden; + width: 460px; +} + +.u-nav .permissions-modal header, +.u-nav .permissions-modal footer { + align-items: center; + background: var(--surface-2); + display: flex; + gap: 8px; + justify-content: space-between; + padding: 8px 10px 8px 12px; +} + +.u-nav .permissions-modal header { + border-bottom: 1px solid var(--line); +} + +.u-nav .permissions-modal footer { + border-top: 1px solid var(--line); + justify-content: flex-end; +} + +.u-nav .modal-body { + display: grid; + gap: 9px; + overflow: auto; + padding: 12px; +} + +.u-nav .permissions-modal input[type="text"], +.u-nav .permissions-modal input:not([type]) { + background: var(--surface-2); + border: 1px solid var(--line); + border-radius: 6px; + color: var(--text); + font: inherit; + min-height: 32px; + padding: 0 9px; + width: 100%; +} + +.u-nav .permissions-modal input:disabled { + opacity: 0.48; +} + +.u-nav .check-row { + align-items: center; + display: flex; + gap: 9px; +} + +.u-nav .check-row input { + accent-color: var(--accent); + height: 18px; + margin: 0; + width: 18px; +} + +.u-nav .check-row.disabled { + opacity: 0.5; +} + +.u-nav .permission-presets { + display: grid; + gap: 6px; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.u-nav .permission-presets button { + min-width: 0; + padding: 0 6px; +} + .u-nav .transfer-panel { background: var(--surface); border: 1px solid var(--line); diff --git a/u-navigator.plg b/u-navigator.plg index 5781309..ee6bdbc 100644 --- a/u-navigator.plg +++ b/u-navigator.plg @@ -1,7 +1,7 @@ - + @@ -24,7 +24,7 @@