Add permissions editor modal

This commit is contained in:
Mikei386
2026-06-23 14:55:59 +02:00
parent 1e20799499
commit 5f014911a5
6 changed files with 216 additions and 19 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
set -eu set -eu
PLUGIN="u-navigator" PLUGIN="u-navigator"
VERSION="2026.06.23.r038" VERSION="2026.06.23.r039"
PKG_DIR="packages" PKG_DIR="packages"
WORK_DIR=".plugin-build" WORK_DIR=".plugin-build"
PKG_NAME="${PLUGIN}-${VERSION}.tgz" PKG_NAME="${PLUGIN}-${VERSION}.tgz"
Binary file not shown.
Binary file not shown.
+112 -16
View File
@@ -11,6 +11,7 @@ const state = {
focusedId: null, focusedId: null,
selectedEntry: null, selectedEntry: null,
contextMenu: null, contextMenu: null,
permissionsDialog: null,
draggedEntry: null, draggedEntry: null,
pointerDrag: null, pointerDrag: null,
suppressClickPath: null, suppressClickPath: null,
@@ -191,6 +192,9 @@ function render() {
if (state.contextMenu) { if (state.contextMenu) {
workspace.appendChild(renderContextMenu()); workspace.appendChild(renderContextMenu());
} }
if (state.permissionsDialog) {
workspace.appendChild(renderPermissionsDialog());
}
renderTransferPanel(); renderTransferPanel();
renderSettingsPanel(); renderSettingsPanel();
} }
@@ -1012,32 +1016,124 @@ async function promptTransfer(win, entry, type) {
await createJob(type, entry.path, `${targetDir.replace(/\/$/, '')}/${entry.name}`, []); await createJob(type, entry.path, `${targetDir.replace(/\/$/, '')}/${entry.name}`, []);
} }
async function promptPermissions(win, entry) { function openPermissionsDialog(win, entry) {
if (isReadOnly()) { if (isReadOnly()) {
showError(new Error('Read-only mode ist aktiv.')); showError(new Error('Read-only mode ist aktiv.'));
return; return;
} }
const entries = selectedPathSet(win).has(entry.path) ? selectedEntries(win) : [entry]; 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) : ''); state.permissionsDialog = {
if (mode === null) return; winId: win.id,
const owner = prompt('Besitzer ändern, leer lassen für unverändert', entry.owner || ''); entries,
if (owner === null) return; mode: entry.permissions ? `0${entry.permissions}`.slice(-4) : '0755',
const group = prompt('Gruppe ändern, leer lassen für unverändert', entry.group || ''); owner: entry.owner || '',
if (group === null) return; group: entry.group || '',
const hasDirectory = entries.some((item) => item.type === 'directory'); recursive: false,
const recursive = hasDirectory && confirm('Rekursiv auf Unterordner und Dateien anwenden?'); 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 = `
<form class="permissions-modal" aria-label="Berechtigungen ändern">
<header>
<strong>Berechtigungen ändern</strong>
<button type="button" class="icon" data-action="cancel" title="Schließen" aria-label="Schließen">×</button>
</header>
<div class="modal-body">
<p class="muted">${dialog.entries.length === 1 ? escapeHtml(dialog.entries[0].name) : `${dialog.entries.length} Objekte ausgewählt`}</p>
<label class="check-row">
<input type="checkbox" name="changeMode" checked>
<span>Modus ändern</span>
</label>
<input name="mode" inputmode="numeric" pattern="[0-7]{3,4}" value="${escapeAttr(dialog.mode)}" aria-label="Oktalmodus">
<div class="permission-presets" aria-label="Schnellauswahl">
<button type="button" data-mode="0644">0644</button>
<button type="button" data-mode="0755">0755</button>
<button type="button" data-mode="0775">0775</button>
<button type="button" data-mode="0777">0777</button>
</div>
<label class="check-row">
<input type="checkbox" name="changeOwner">
<span>Besitzer ändern</span>
</label>
<input name="owner" value="${escapeAttr(dialog.owner)}" aria-label="Besitzer" disabled>
<label class="check-row">
<input type="checkbox" name="changeGroup">
<span>Gruppe ändern</span>
</label>
<input name="group" value="${escapeAttr(dialog.group)}" aria-label="Gruppe" disabled>
<label class="check-row ${dialog.hasDirectory ? '' : 'disabled'}">
<input type="checkbox" name="recursive" ${dialog.hasDirectory ? '' : 'disabled'}>
<span>Rekursiv auf Unterordner und Dateien anwenden</span>
</label>
</div>
<footer>
<button type="button" data-action="cancel">Abbrechen</button>
<button type="submit" class="primary">Anwenden</button>
</footer>
</form>
`;
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 = { const options = {
mode: mode.trim(), mode: form.elements.changeMode.checked ? form.elements.mode.value.trim() : '',
owner: owner.trim(), owner: form.elements.changeOwner.checked ? form.elements.owner.value.trim() : '',
group: group.trim(), group: form.elements.changeGroup.checked ? form.elements.group.value.trim() : '',
recursive: recursive ? '1' : '0' recursive: form.elements.recursive.checked ? '1' : '0'
}; };
if (!options.mode && !options.owner && !options.group) { if (!options.mode && !options.owner && !options.group) {
showError(new Error('Keine Änderung angegeben.')); showError(new Error('Keine Änderung angegeben.'));
return; return;
} }
for (const item of entries) { if (options.mode && !/^[0-7]{3,4}$/.test(options.mode)) {
await createJob('permissions', item.path, null, [win.id], options); 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)]); actions.push(['Eigenschaften', () => showPropertiesForEntry(win, entry)]);
if (!isReadOnly()) { if (!isReadOnly()) {
actions.push(['Umbenennen', () => renameEntry(win, entry)]); 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(['Kopieren nach...', () => promptTransfer(win, entry, 'copy')]);
actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]); actions.push(['Verschieben nach...', () => promptTransfer(win, entry, 'move')]);
} }
+101
View File
@@ -51,6 +51,12 @@
border-color: var(--accent); border-color: var(--accent);
} }
.u-nav button.primary {
background: var(--accent);
border-color: var(--accent);
color: #ffffff;
}
.u-nav button.icon { .u-nav button.icon {
align-items: center; align-items: center;
display: inline-flex; display: inline-flex;
@@ -496,6 +502,101 @@
border-color: var(--line); 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 { .u-nav .transfer-panel {
background: var(--surface); background: var(--surface);
border: 1px solid var(--line); border: 1px solid var(--line);
+2 -2
View File
@@ -1,7 +1,7 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "u-navigator"> <!ENTITY name "u-navigator">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.06.23.r038"> <!ENTITY version "2026.06.23.r039">
<!ENTITY package "&name;-&version;.tgz"> <!ENTITY package "&name;-&version;.tgz">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/u-navigator.plg">
<!ENTITY support "https://git.casaderoll.de/michael/Unraid-Navigator"> <!ENTITY support "https://git.casaderoll.de/michael/Unraid-Navigator">
@@ -24,7 +24,7 @@
<FILE Name="/boot/config/plugins/&name;/&package;"> <FILE Name="/boot/config/plugins/&name;/&package;">
<URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL> <URL>https://git.casaderoll.de/michael/Unraid-Navigator/raw/branch/main/packages/&package;</URL>
<MD5>87661843082c0f41bd627a315278e346</MD5> <MD5>bee22dc7b1842112f37fe2513fd413d0</MD5>
</FILE> </FILE>
<FILE Run="/bin/bash"> <FILE Run="/bin/bash">