Add permissions editor modal
This commit is contained in:
+1
-1
@@ -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
@@ -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')]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user