using dialogs in file browser now

This commit is contained in:
Ludwig Lehnert
2026-01-12 21:00:07 +01:00
parent 1f670df447
commit 5b4693ea9a

View File

@@ -317,7 +317,7 @@ function renderFileManagerPage(title, body) {
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>${title}</title> <title>${title}</title>
<style> <style>
:root { --ink:#0b0f19; --muted:#5b6470; --line:#d8dde4; --bg:#eef1f5; --card:#ffffff; --accent:#0f766e; --accent-strong:#0a5b55; } :root { --ink:#0b0f19; --muted:#5b6470; --line:#d8dde4; --bg:#eef1f5; --card:#ffffff; --accent:#0f766e; --accent-strong:#0a5b55; --accent-soft:#e6f4f2; }
* { box-sizing: border-box; } * { box-sizing: border-box; }
body { margin: 0; font-family: "IBM Plex Sans", "Noto Sans", sans-serif; background: var(--bg); color: var(--ink); } body { margin: 0; font-family: "IBM Plex Sans", "Noto Sans", sans-serif; background: var(--bg); color: var(--ink); }
main { max-width: 1280px; margin: 0 auto; padding: 26px 18px 70px; } main { max-width: 1280px; margin: 0 auto; padding: 26px 18px 70px; }
@@ -327,8 +327,14 @@ function renderFileManagerPage(title, body) {
.muted { color: var(--muted); font-size: 0.95rem; } .muted { color: var(--muted); font-size: 0.95rem; }
.card { margin-top: 18px; padding: 16px; background: var(--card); border-radius: 16px; border: 1px solid var(--line); box-shadow: 0 8px 26px rgba(12, 18, 28, 0.08); } .card { margin-top: 18px; padding: 16px; background: var(--card); border-radius: 16px; border: 1px solid var(--line); box-shadow: 0 8px 26px rgba(12, 18, 28, 0.08); }
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } .toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.tag { display: inline-flex; align-items: center; gap: 8px; padding: 4px 12px; border-radius: 999px; background: #f1f5f9; border: 1px solid var(--line); color: var(--ink); text-decoration: none; font-weight: 600; } .tag { display: inline-flex; align-items: center; gap: 8px; padding: 6px 14px; border-radius: 999px; background: #f1f5f9; border: 1px solid var(--line); color: var(--ink); text-decoration: none; font-weight: 600; }
.tag.primary { background: var(--accent-soft); border-color: #bfe6e0; color: var(--accent-strong); }
.tag span { color: var(--muted); font-weight: 500; } .tag span { color: var(--muted); font-weight: 500; }
.browser-shell { display: grid; gap: 16px; }
.browser-bar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.crumbs { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.crumbs a { color: var(--accent-strong); text-decoration: none; font-weight: 600; }
.crumbs span { color: var(--muted); }
.grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); } .grid { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); }
form { display: grid; gap: 10px; } form { display: grid; gap: 10px; }
label { display: grid; gap: 6px; font-weight: 600; } label { display: grid; gap: 6px; font-weight: 600; }
@@ -339,12 +345,17 @@ function renderFileManagerPage(title, body) {
button.secondary { background: transparent; color: var(--accent); } button.secondary { background: transparent; color: var(--accent); }
table { width: 100%; border-collapse: collapse; font-size: 0.95rem; } table { width: 100%; border-collapse: collapse; font-size: 0.95rem; }
th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; } th, td { text-align: left; padding: 10px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
tbody tr:nth-child(even) { background: #f9fbfc; } tbody tr:hover { background: #f2f8f7; }
.name { display: inline-flex; align-items: center; gap: 10px; } .name { display: inline-flex; align-items: center; gap: 10px; }
.name strong { font-weight: 700; } .name strong { font-weight: 700; }
.folder { font-weight: 700; color: var(--accent-strong); text-decoration: none; } .folder { font-weight: 700; color: var(--accent-strong); text-decoration: none; }
.actions { display: grid; gap: 6px; } .actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.actions input, .actions button { font-size: 0.9rem; padding: 7px 10px; } .actions button { font-size: 0.9rem; padding: 7px 10px; }
dialog { border: none; border-radius: 16px; padding: 0; width: min(480px, 92vw); }
dialog::backdrop { background: rgba(15, 23, 42, 0.4); }
.dialog-card { padding: 18px; display: grid; gap: 12px; }
.dialog-actions { display: flex; gap: 10px; justify-content: flex-end; }
.dialog-actions .secondary { border-color: var(--line); color: var(--accent-strong); }
</style> </style>
</head> </head>
<body> <body>
@@ -743,8 +754,8 @@ app.get(`${basePath}/admin/dashboard`, requireAdminPage, async (req, res) => {
<div class="muted">Systemstatistiken und Logs</div> <div class="muted">Systemstatistiken und Logs</div>
</div> </div>
<div class="toolbar"> <div class="toolbar">
<a class="tag" href="${baseUrl('/admin/files')}">Dateimanager</a> <a class="tag primary" href="${baseUrl('/admin/files')}">Dateimanager</a>
<a class="tag" href="${baseUrl('/admin/users')}">Benutzer verwalten</a> <a class="tag primary" href="${baseUrl('/admin/users')}">Benutzer verwalten</a>
<form method="post" action="${baseUrl('/admin/logout')}"> <form method="post" action="${baseUrl('/admin/logout')}">
${csrfField(res.locals.csrfToken)} ${csrfField(res.locals.csrfToken)}
<button type="submit" class="secondary">Abmelden</button> <button type="submit" class="secondary">Abmelden</button>
@@ -937,11 +948,31 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
const parentPath = relativePath ? relativePath.split('/').slice(0, -1).join('/') : ''; const parentPath = relativePath ? relativePath.split('/').slice(0, -1).join('/') : '';
const dirs = entries.filter((entry) => entry.isDirectory() && entry.name !== '_share'); const filtered = entries.filter((entry) => entry.name !== '_share');
const files = entries.filter((entry) => !entry.isDirectory() && entry.name !== '_share'); const details = await Promise.all(
filtered.map(async (entry) => {
const rowForEntry = (entry, isDir) => {
const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; const childPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
let stat = null;
try {
stat = await fs.promises.stat(path.join(resolved, entry.name));
} catch (err) {
stat = null;
}
return {
entry,
childPath,
isDir: entry.isDirectory(),
size: stat && stat.isFile() ? stat.size : null,
modifiedAt: stat ? stat.mtimeMs : null,
};
})
);
const dirs = details.filter((item) => item.isDir);
const files = details.filter((item) => !item.isDir);
const rowForEntry = (item) => {
const { entry, childPath, isDir, size, modifiedAt } = item;
const href = baseUrl(`/admin/files?path=${encodeURIComponent(childPath)}`); const href = baseUrl(`/admin/files?path=${encodeURIComponent(childPath)}`);
const escapedName = escapeHtml(entry.name); const escapedName = escapeHtml(entry.name);
const escapedPath = escapeHtml(childPath); const escapedPath = escapeHtml(childPath);
@@ -954,26 +985,19 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
</span> </span>
</td> </td>
<td>${isDir ? 'Ordner' : 'Datei'}</td> <td>${isDir ? 'Ordner' : 'Datei'}</td>
<td>${size ? formatBytes(size) : '—'}</td>
<td>${modifiedAt ? formatTimestamp(modifiedAt) : '—'}</td>
<td class="actions"> <td class="actions">
<form method="post" action="${baseUrl('/admin/files/rename')}"> <button type="button" class="secondary rename-trigger" data-path="${escapedPath}" data-name="${escapedName}">Umbenennen</button>
${csrfField(res.locals.csrfToken)} <button type="button" class="secondary delete-trigger" data-path="${escapedPath}" data-name="${escapedName}">Löschen</button>
<input type="hidden" name="path" value="${escapedPath}" />
<input name="newName" placeholder="Neuer Name" required />
<button type="submit">Umbenennen</button>
</form>
<form method="post" action="${baseUrl('/admin/files/delete')}">
${csrfField(res.locals.csrfToken)}
<input type="hidden" name="path" value="${escapedPath}" />
<button type="submit" class="secondary">Löschen</button>
</form>
</td> </td>
</tr> </tr>
`; `;
}; };
const tableRows = [ const tableRows = [
...dirs.map((entry) => rowForEntry(entry, true)), ...dirs.map((entry) => rowForEntry(entry)),
...files.map((entry) => rowForEntry(entry, false)), ...files.map((entry) => rowForEntry(entry)),
].join(''); ].join('');
const body = ` const body = `
@@ -991,14 +1015,19 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
</div> </div>
</header> </header>
<section class="card"> <section class="card browser-shell">
<div class="toolbar"> <div class="browser-bar">
<span class="tag">Pfad <span>/${escapeHtml(relativePath || '')}</span></span> <span class="tag">Pfad <span>/${escapeHtml(relativePath || '')}</span></span>
${relativePath ? `<a class="tag" href="${baseUrl(`/admin/files?path=${encodeURIComponent(parentPath)}`)}">&larr; Zurück</a>` : ''} ${relativePath ? `<a class="tag primary" href="${baseUrl(`/admin/files?path=${encodeURIComponent(parentPath)}`)}">&larr; Zurück</a>` : ''}
<div class="crumbs">
<span>Position:</span>
${relativePath ? relativePath.split('/').map((segment, idx, parts) => {
const crumbPath = parts.slice(0, idx + 1).join('/');
return `<a href="${baseUrl(`/admin/files?path=${encodeURIComponent(crumbPath)}`)}">${escapeHtml(segment)}</a>`;
}).join('<span>/</span>') : '<span>Root</span>'}
</div> </div>
</section> </div>
<div class="grid">
<section class="card grid">
<div> <div>
<h2>Ordner erstellen</h2> <h2>Ordner erstellen</h2>
<form method="post" action="${baseUrl('/admin/files/mkdir')}"> <form method="post" action="${baseUrl('/admin/files/mkdir')}">
@@ -1023,6 +1052,7 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
<button type="submit">Hochladen</button> <button type="submit">Hochladen</button>
</form> </form>
</div> </div>
</div>
</section> </section>
<section class="card"> <section class="card">
@@ -1033,6 +1063,8 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Typ</th> <th>Typ</th>
<th>Größe</th>
<th>Geändert</th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
</thead> </thead>
@@ -1042,6 +1074,70 @@ app.get(`${basePath}/admin/files`, requireAdminPage, async (req, res) => {
</table> </table>
` : '<div class="muted">Keine Eintraege in diesem Ordner.</div>'} ` : '<div class="muted">Keine Eintraege in diesem Ordner.</div>'}
</section> </section>
<dialog id="rename-dialog">
<form method="post" action="${baseUrl('/admin/files/rename')}" class="dialog-card">
${csrfField(res.locals.csrfToken)}
<input type="hidden" name="path" id="rename-path" />
<h2>Umbenennen</h2>
<div class="muted" id="rename-current"></div>
<label>
Neuer Name
<input name="newName" id="rename-name" required />
</label>
<div class="dialog-actions">
<button type="button" class="secondary dialog-close">Abbrechen</button>
<button type="submit">Speichern</button>
</div>
</form>
</dialog>
<dialog id="delete-dialog">
<form method="post" action="${baseUrl('/admin/files/delete')}" class="dialog-card">
${csrfField(res.locals.csrfToken)}
<input type="hidden" name="path" id="delete-path" />
<h2>Löschen</h2>
<div class="muted" id="delete-current"></div>
<div class="dialog-actions">
<button type="button" class="secondary dialog-close">Abbrechen</button>
<button type="submit">Löschen</button>
</div>
</form>
</dialog>
<script>
const renameDialog = document.getElementById('rename-dialog');
const deleteDialog = document.getElementById('delete-dialog');
const renamePath = document.getElementById('rename-path');
const renameName = document.getElementById('rename-name');
const renameCurrent = document.getElementById('rename-current');
const deletePath = document.getElementById('delete-path');
const deleteCurrent = document.getElementById('delete-current');
document.querySelectorAll('.rename-trigger').forEach((btn) => {
btn.addEventListener('click', () => {
renamePath.value = btn.dataset.path || '';
renameName.value = btn.dataset.name || '';
renameCurrent.textContent = `Aktuell: ${btn.dataset.name || ''}`;
renameDialog.showModal();
});
});
document.querySelectorAll('.delete-trigger').forEach((btn) => {
btn.addEventListener('click', () => {
deletePath.value = btn.dataset.path || '';
deleteCurrent.textContent = `Datei/Ordner: ${btn.dataset.name || ''}`;
deleteDialog.showModal();
});
});
document.querySelectorAll('.dialog-close').forEach((btn) => {
btn.addEventListener('click', () => {
renameDialog.close();
deleteDialog.close();
});
});
</script>
`; `;
res.send(renderFileManagerPage('Admin-Dateien', body)); res.send(renderFileManagerPage('Admin-Dateien', body));