Add settings panel and readonly default

This commit is contained in:
Mikei386
2026-06-23 13:06:12 +02:00
parent 775d979429
commit 2d56f57c54
9 changed files with 254 additions and 16 deletions
+132 -9
View File
@@ -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) {
<div class="pathbar">
<button class="icon up-button" data-action="up" title="Eine Ebene zurueck" aria-label="Eine Ebene zurueck">←</button>
<input value="${escapeAttr(data.path || '')}" aria-label="Pfad">
<button data-action="go">Öffnen</button>
<button data-action="refresh">Neu laden</button>
<button class="icon" data-action="go" title="Öffnen" aria-label="Öffnen">↵</button>
<button class="icon" data-action="refresh" title="Neu laden" aria-label="Neu laden">↻</button>
<div class="view-toggle" aria-label="Ansicht">
<button class="${data.view !== 'icons' ? 'active' : ''}" data-action="view-list" title="Listenansicht">Liste</button>
<button class="${data.view === 'icons' ? 'active' : ''}" data-action="view-icons" title="Symbolansicht">Symbole</button>
<button class="icon ${data.view !== 'icons' ? 'active' : ''}" data-action="view-list" title="Listenansicht" aria-label="Listenansicht">☰</button>
<button class="icon ${data.view === 'icons' ? 'active' : ''}" data-action="view-icons" title="Symbolansicht" aria-label="Symbolansicht">▦</button>
</div>
</div>
<div class="drop-zone ${data.dragOver ? 'drag-over' : ''}">
@@ -378,6 +382,15 @@ function queueMarkup() {
<div class="progress"><div style="width:${Number(job.progress) || 0}%"></div></div>
</article>
`).join('') : '<p class="muted">Keine Transfers.</p>';
const debugSection = state.config?.debug ? debugMarkup() : '';
return `
<div class="queue-list">${jobMarkup}</div>
${debugSection}
`;
}
function debugMarkup() {
const debugMarkup = state.debug.length ? state.debug.slice(-12).reverse().map((item) => `
<article class="debug-entry">
<strong>#${item.id} ${escapeHtml(item.label)}</strong>
@@ -387,7 +400,6 @@ function queueMarkup() {
`).join('') : '<p class="muted">Noch keine Debug-Einträge.</p>';
return `
<div class="queue-list">${jobMarkup}</div>
<section class="debug-log" aria-label="Debug-Protokoll">
<header>
<strong>Debug</strong>
@@ -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 = `
<header class="settings-panel-header">
<strong>Einstellungen</strong>
<button type="button" class="icon" data-action="close-settings" title="Schließen">×</button>
</header>
<div class="settings-panel-body">
<label class="setting-row">
<span>
<strong>Debug</strong>
<small>Debug-Protokoll im Transfers-Panel anzeigen</small>
</span>
<input type="checkbox" data-setting="debug" ${state.config?.debug ? 'checked' : ''}>
</label>
<label class="setting-row">
<span>
<strong>Schreibzugriff</strong>
<small>Upload, Rename, Move und Copy erlauben</small>
</span>
<input type="checkbox" data-setting="write" ${state.config && !state.config.readOnly ? 'checked' : ''}>
</label>
</div>
`;
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(),